. */ namespace Tests\Feature; use App\Account; use App\Password; 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; use Tests\TestCase; class AccountJWTAuthenticationTest extends TestCase { protected $route = '/provisioning'; protected $accountRoute = '/provisioning/me'; protected $method = 'GET'; protected $serverPrivateKeyPem = null; protected $serverPublicKeyPem = null; protected $routeAccountMe = '/api/accounts/me'; public function setUp(): void { parent::setUp(); $keys = openssl_pkey_new(array("private_key_bits" => 4096,"private_key_type" => OPENSSL_KEYTYPE_RSA)); $this->serverPublicKeyPem = openssl_pkey_get_details($keys)['key']; openssl_pkey_export($keys, $this->serverPrivateKeyPem); } public function testBaseProvisioning() { # JWT is disabled if Sodium is not loaded if (!extension_loaded('sodium')) return; $password = Password::factory()->create(); $domain = 'sip_provisioning.example.com'; $bearer = 'authz_server="https://sso.test/", realm="sip.test.org"'; \App\Space::where('domain', $password->account->domain)->update(['host' => $domain]); config()->set('app.sip_domain', $domain); config()->set('services.jwt.rsa_public_key_pem', $this->serverPublicKeyPem); $this->get($this->route)->assertStatus(400); $clock = new FrozenClock(new DateTimeImmutable()); $token = (new JwtFacade(null, $clock))->issue( new Sha256(), InMemory::plainText($this->serverPrivateKeyPem), static fn ( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('email', $password->account->email) ); $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( new Sha512(), InMemory::plainText($this->serverPrivateKeyPem), static fn ( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('email', $password->account->email) ); $this->checkToken($token); // Expired token $oldClock = new FrozenClock(new DateTimeImmutable('2022-06-24 22:51:10')); $token = (new JwtFacade(null, $oldClock))->issue( new Sha256(), InMemory::plainText($this->serverPrivateKeyPem), static fn ( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('email', $password->account->email) ); $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $token->toString(), 'x-linphone-provisioning' => true, ]) ->get($this->accountRoute) ->assertStatus(401); $this->assertStringContainsString('invalid_token', $response->headers->get('WWW-Authenticate')); // ...with the bearer config()->set('app.account_authentication_bearer', $bearer); $response = $this->withHeaders([ 'Authorization' => 'Bearer ' . $token->toString(), 'x-linphone-provisioning' => true, ]) ->get($this->accountRoute) ->assertStatus(401); $this->assertStringContainsString($bearer . ', ', $response->headers->get('WWW-Authenticate')); $this->assertStringContainsString('invalid_token', $response->headers->get('WWW-Authenticate')); // Wrong email $token = (new JwtFacade(null, $clock))->issue( new Sha256(), InMemory::plainText($this->serverPrivateKeyPem), static fn ( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('email', 'unknow@man.org') ); $this->withHeaders([ 'Authorization' => 'Bearer ' . $token->toString(), 'x-linphone-provisioning' => true, ]) ->get($this->accountRoute) ->assertStatus(403); // Wrong signature key $keys = openssl_pkey_new(array("private_key_bits" => 4096,"private_key_type" => OPENSSL_KEYTYPE_RSA)); openssl_pkey_export($keys, $wrongServerPrivateKeyPem); $wrongToken = (new JwtFacade(null, $clock))->issue( new Sha256(), InMemory::plainText($wrongServerPrivateKeyPem), static fn ( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('email', $password->account->email) ); $this->withHeaders([ 'Authorization' => 'Bearer ' . $wrongToken->toString(), 'x-linphone-provisioning' => true, ]) ->get($this->accountRoute) ->assertStatus(401); } public function testAuthBearerUrl() { $value = 'authz_server="https://auth_bearer.com/" realm="realm"'; config()->set('app.account_authentication_bearer', $value); $password = Password::factory()->create(); $response = $this->json($this->method, $this->routeAccountMe) ->assertStatus(401); $this->assertStringContainsString( 'Bearer ' . $value, $response->headers->all()['www-authenticate'][0] ); // Wrong From $reponse = $this ->withHeaders(['From' => 'sip:missing@username']) ->json($this->method, $this->routeAccountMe) ->assertStatus(401); $this->assertStringContainsString( 'Bearer ' . $value, $response->headers->all()['www-authenticate'][0] ); // Wrong bearer message $reponse = $this ->withHeaders([ 'Authorization' => 'Bearer 1234' ]) ->json($this->method, $this->routeAccountMe) ->assertStatus(401); $this->assertStringContainsString( 'Bearer ' . $value, $response->headers->all()['www-authenticate'][0] ); } 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'); } }