Fix FLEXIAPI-167 Add the handling of a custom identifier for the JWT tokens on top of the email one

This commit is contained in:
Timothée Jaussoin 2024-04-30 10:10:19 +00:00
parent c13a78002a
commit 4f79ddca2b
9 changed files with 86 additions and 39 deletions

View file

@ -3,6 +3,7 @@
v1.5
----
- Fix FLEXIAPI-168 Add POST /accounts/me/email to confirm the email change
- Fix FLEXIAPI-167 Add the handling of a custom identifier for the JWT tokens on top of the email one
- Fix FLEXIAPI-166 Reimplement the deprecated email validation URL
- Fix FLEXIAPI-165 Remove for now text/vcard header constraint
- Fix FLEXIAPI-164 Add vcards-storage endpoints

View file

@ -114,3 +114,4 @@ HCAPTCHA_SITEKEY=site-key
# JWT
JWT_RSA_PUBLIC_KEY_PEM=
JWT_SIP_IDENTIFIER=

View file

@ -22,7 +22,6 @@ use Illuminate\Support\Str;
use App\Account;
use App\DigestNonce;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
@ -51,23 +50,23 @@ function generateValidNonce(Account $account): string
return $nonce->nonce;
}
function bchash(string $username, string $domain, string $password, string $algorithm = 'MD5')
function bchash(string $username, string $domain, string $password, string $algorithm = 'MD5'): string
{
return hash(passwordAlgorithms()[$algorithm], $username . ':' . $domain . ':' . $password);
}
function generatePin()
function generatePin(): int
{
return mt_rand(1000, 9999);
}
function percent($value, $max)
function percent($value, $max): float
{
if ($max == 0) $max = 1;
return round(($value * 100) / $max, 2);
}
function markdownDocumentationView($view): string
function markdownDocumentationView(string $view): string
{
$converter = new CommonMarkConverter([
'heading_permalink' => [
@ -92,7 +91,12 @@ function markdownDocumentationView($view): string
);
}
function isRegularExpression($string): bool
function parseSIP(string $sipAdress): array
{
return explode('@', \substr($sipAdress, 4));
}
function isRegularExpression(string $string): bool
{
set_error_handler(function () {
}, E_WARNING);

View file

@ -70,7 +70,7 @@ class AuthenticateDigestOrKey
])->validate();
$from = $this->extractFromHeader($request->header('From'));
list($username, $domain) = explode('@', $from);
list($username, $domain) = parseSIP($from);
$account = Account::withoutGlobalScopes()
->where('username', $username)
@ -234,7 +234,7 @@ class AuthenticateDigestOrKey
private function extractFromHeader(string $string): string
{
list($from) = explode(';', \substr($string, 4));
list($from) = explode(';', $string);
return \rawurldecode($from);
}

View file

@ -33,15 +33,15 @@ class AuthenticateJWT
switch ($token->headers()->get('alg')) {
case 'RS256':
$signer = new Sha256;
$signer = new Sha256();
break;
case 'RS384':
$signer = new Sha384;
$signer = new Sha384();
break;
case 'RS512':
$signer = new Sha512;
$signer = new Sha512();
break;
}
@ -57,9 +57,20 @@ class AuthenticateJWT
abort(403, 'Expired JWT token');
}
$account = Account::withoutGlobalScopes()
->where('email', $token->claims()->get('email'))
->first();
$account = null;
if ($token->claims()->has(config('services.jwt.sip_identifier'))) {
list($username, $domain) = parseSIP($token->claims()->get(config('services.jwt.sip_identifier')));
$account = Account::withoutGlobalScopes()
->where('username', $username)
->where('domain', $domain)
->first();
} elseif ($token->claims()->has('email')) {
$account = Account::withoutGlobalScopes()
->where('email', $token->claims()->get('email'))
->first();
}
if (!$account) {
abort(403, 'The JWT token is not related to someone in the system');

14
flexiapi/composer.lock generated
View file

@ -4738,20 +4738,20 @@
},
{
"name": "ramsey/uuid",
"version": "4.7.5",
"version": "4.7.6",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e"
"reference": "91039bc1faa45ba123c4328958e620d382ec7088"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e",
"reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088",
"reference": "91039bc1faa45ba123c4328958e620d382ec7088",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11",
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12",
"ext-json": "*",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
@ -4814,7 +4814,7 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.7.5"
"source": "https://github.com/ramsey/uuid/tree/4.7.6"
},
"funding": [
{
@ -4826,7 +4826,7 @@
"type": "tidelift"
}
],
"time": "2023-11-08T05:53:05+00:00"
"time": "2024-04-27T21:32:50+00:00"
},
{
"name": "react/cache",

Binary file not shown.

View file

@ -32,6 +32,7 @@ return [
'jwt' => [
'rsa_public_key_pem' => env('JWT_RSA_PUBLIC_KEY_PEM'),
],
'sip_identifier' => env('JWT_SIP_IDENTIFIER', 'sip_identity'),
],
];

View file

@ -24,6 +24,7 @@ use DateTimeImmutable;
use Lcobucci\Clock\FrozenClock;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\JwtFacade;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Signer\Rsa\Sha512;
@ -68,14 +69,38 @@ class AccountJWTAuthenticationTest extends TestCase
): Builder => $builder->withClaim('email', $password->account->email)
);
$this->withHeaders([
'Authorization' => 'Bearer ' . $token->toString(),
'x-linphone-provisioning' => true,
])
->get($this->accountRoute)
->assertStatus(200)
->assertHeader('Content-Type', 'application/xml')
->assertSee('ha1');
$this->checkToken($token);
// SIP identifier
// This line shoudn't be required, but the pipeline doesn't get the default value somehow
config()->set('services.jwt.sip_identifier', 'sip_identity');
$token = (new JwtFacade(null, $clock))->issue(
new Sha256(),
InMemory::plainText($this->serverPrivateKeyPem),
static fn (
Builder $builder,
DateTimeImmutable $issuedAt
): Builder => $builder->withClaim('sip_identity', 'sip:' . $password->account->username . '@' . $password->account->domain)
);
$this->checkToken($token);
// Custom SIP identifier
$otherIdentifier = 'sip_other_identifier';
config()->set('services.jwt.sip_identifier', $otherIdentifier);
$token = (new JwtFacade(null, $clock))->issue(
new Sha256(),
InMemory::plainText($this->serverPrivateKeyPem),
static fn (
Builder $builder,
DateTimeImmutable $issuedAt
): Builder => $builder->withClaim($otherIdentifier, 'sip:' . $password->account->username . '@' . $password->account->domain)
);
$this->checkToken($token);
// Sha512
$token = (new JwtFacade(null, $clock))->issue(
@ -87,17 +112,9 @@ class AccountJWTAuthenticationTest extends TestCase
): Builder => $builder->withClaim('email', $password->account->email)
);
$this->withHeaders([
'Authorization' => 'Bearer ' . $token->toString(),
'x-linphone-provisioning' => true,
])
->get($this->accountRoute)
->assertStatus(200)
->assertHeader('Content-Type', 'application/xml')
->assertSee('ha1');
$this->checkToken($token);
// Expired token
$oldClock = new FrozenClock(new DateTimeImmutable('2022-06-24 22:51:10'));
$token = (new JwtFacade(null, $oldClock))->issue(
@ -155,4 +172,16 @@ class AccountJWTAuthenticationTest extends TestCase
->get($this->accountRoute)
->assertStatus(403);
}
private function checkToken(UnencryptedToken $token): void
{
$this->withHeaders([
'Authorization' => 'Bearer ' . $token->toString(),
'x-linphone-provisioning' => true,
])
->get($this->accountRoute)
->assertStatus(200)
->assertHeader('Content-Type', 'application/xml')
->assertSee('ha1');
}
}