diff --git a/CHANGELOG.md b/CHANGELOG.md index 646f9fd..2b4ded9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/flexiapi/.env.example b/flexiapi/.env.example index 3e4a203..ab60b93 100644 --- a/flexiapi/.env.example +++ b/flexiapi/.env.example @@ -114,3 +114,4 @@ HCAPTCHA_SITEKEY=site-key # JWT JWT_RSA_PUBLIC_KEY_PEM= +JWT_SIP_IDENTIFIER= diff --git a/flexiapi/app/Helpers/Utils.php b/flexiapi/app/Helpers/Utils.php index 6c3ab34..0c4293d 100644 --- a/flexiapi/app/Helpers/Utils.php +++ b/flexiapi/app/Helpers/Utils.php @@ -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); diff --git a/flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php b/flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php index 0930b2c..83913ee 100644 --- a/flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php +++ b/flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php @@ -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); } diff --git a/flexiapi/app/Http/Middleware/AuthenticateJWT.php b/flexiapi/app/Http/Middleware/AuthenticateJWT.php index 6009c6c..d9e4398 100644 --- a/flexiapi/app/Http/Middleware/AuthenticateJWT.php +++ b/flexiapi/app/Http/Middleware/AuthenticateJWT.php @@ -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'); diff --git a/flexiapi/composer.lock b/flexiapi/composer.lock index e4c5c5e..6b0641c 100644 --- a/flexiapi/composer.lock +++ b/flexiapi/composer.lock @@ -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", diff --git a/flexiapi/composer.phar b/flexiapi/composer.phar index e6ba7bb..9b28eb4 100755 Binary files a/flexiapi/composer.phar and b/flexiapi/composer.phar differ diff --git a/flexiapi/config/services.php b/flexiapi/config/services.php index 0750948..d487efc 100644 --- a/flexiapi/config/services.php +++ b/flexiapi/config/services.php @@ -32,6 +32,7 @@ return [ 'jwt' => [ 'rsa_public_key_pem' => env('JWT_RSA_PUBLIC_KEY_PEM'), - ], + 'sip_identifier' => env('JWT_SIP_IDENTIFIER', 'sip_identity'), + ], ]; diff --git a/flexiapi/tests/Feature/AccountJWTAuthenticationTest.php b/flexiapi/tests/Feature/AccountJWTAuthenticationTest.php index e4162d0..7bd62c2 100644 --- a/flexiapi/tests/Feature/AccountJWTAuthenticationTest.php +++ b/flexiapi/tests/Feature/AccountJWTAuthenticationTest.php @@ -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'); + } }