From 0669b0d965d42cbd0596dce8a36539ec8f4fe9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Jaussoin?= Date: Thu, 30 Apr 2026 14:09:10 +0000 Subject: [PATCH] Fix FLEXIAPI-464 Add per Space custom RFC8898 flow --- CHANGELOG.md | 16 +- flexiapi/.env.example | 5 - .../Admin/Space/SSOServerController.php | 56 ++++ .../app/Http/Middleware/AuthenticateJWT.php | 30 +- flexiapi/app/Space.php | 47 ++- flexiapi/composer.json | 1 + flexiapi/composer.lock | 275 +++++++++++++++++- flexiapi/config/app.php | 1 - flexiapi/config/services.php | 6 - ...140240_add_sso_configuration_to_spaces.php | 28 ++ flexiapi/lang/fr.json | 8 + flexiapi/licenses.md | 164 ++++++++++- .../views/admin/space/integration.blade.php | 28 +- .../admin/space/sso_server/show.blade.php | 57 ++++ flexiapi/routes/web.php | 7 +- .../Feature/AccountJWTAuthenticationTest.php | 99 ++++--- 16 files changed, 723 insertions(+), 105 deletions(-) create mode 100644 flexiapi/app/Http/Controllers/Admin/Space/SSOServerController.php create mode 100644 flexiapi/database/migrations/2026_04_21_140240_add_sso_configuration_to_spaces.php create mode 100644 flexiapi/resources/views/admin/space/sso_server/show.blade.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 17b4f4b..a60524b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Added -- **Add CardDav servers** They can be configured in the administration panels and the API. +- **CardDav servers** They can be configured in the administration panels and the API. - **Rockylinux 10 support** Packages are now available in the official repository - **Artisan cleanup script for statistics** Add an artisan console script to clear statistics after n days `app:clear-statistics {days} {--apply}` -- **Add Voicemail features and related API endpoints** to integrate with `flexisip-voicemail` -- **Add Call Forwarding features and related API endpoints** -- **Add generated licenses.md file in FlexiAPI** +- **Voicemail features and related API endpoints** to integrate with `flexisip-voicemail` +- **Call Forwarding features and related API endpoints** +- **Generated licenses.md file in FlexiAPI** +- **Per Space Custom SSO authentication flow** ### Changed @@ -21,6 +22,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - **PHP 8.2 minimum** Laravel and its dependencies were upgraded to version 11 as well. - **Logout the user when the password is correctly changed** +### Removed + +- **Space SSO authentication:** Custom SSO authentication support has now be moved into Spaces configuraton, the following environnement keys are removed. + - `JWT_RSA_PUBLIC_KEY_PEM` + - `JWT_SIP_IDENTIFIER` + - `ACCOUNT_AUTHENTICATION_BEARER`, it is now generated directly from the Space configuration + ## [2.0] ### Added diff --git a/flexiapi/.env.example b/flexiapi/.env.example index 549ee7a..3046ba8 100644 --- a/flexiapi/.env.example +++ b/flexiapi/.env.example @@ -29,7 +29,6 @@ ACCOUNT_EMAIL_UNIQUE=false # Emails are unique between all the accounts ACCOUNT_BLACKLISTED_USERNAMES= ACCOUNT_USERNAME_REGEX="^[a-z0-9+_.-]*$" ACCOUNT_DEFAULT_PASSWORD_ALGORITHM=SHA-256 # Can ONLY be MD5 or SHA-256 in capital, default to SHA-256 -ACCOUNT_AUTHENTICATION_BEARER= # Bearer value (WWW-Authenticate: Bearer ) of the external service that can provide a trusted (eg. JWT token) for the authentication, takes priority and disable the DIGEST auth if set, see https://www.rfc-editor.org/rfc/rfc8898 # Blocking service BLOCKING_TIME_PERIOD_CHECK=30 # Time span on which the blocking service will proceed, in minutes @@ -93,9 +92,5 @@ OVH_APP_SENDER= HCAPTCHA_SECRET=secret-key HCAPTCHA_SITEKEY=site-key -# JWT -JWT_RSA_PUBLIC_KEY_PEM= -JWT_SIP_IDENTIFIER= - # Temporary toggles APP_SHOW_LOGIN_COUNTER_TEMP= # default true \ No newline at end of file diff --git a/flexiapi/app/Http/Controllers/Admin/Space/SSOServerController.php b/flexiapi/app/Http/Controllers/Admin/Space/SSOServerController.php new file mode 100644 index 0000000..794a929 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Admin/Space/SSOServerController.php @@ -0,0 +1,56 @@ + Space::findOrFail($spaceId) + ]); + } + + public function refreshPublicKey(int $spaceId) + { + $space = Space::findOrFail($spaceId); + + if (!$space->refreshSSOCertificate()) { + return redirect()->back()->withErrors([ + 'sso_public_key' => __('The public key cannot be refreshed') + ]); + } + + $space->save(); + + return redirect()->back(); + } + + public function store(Request $request, int $spaceId) + { + $request->validate([ + 'sso_server_url' => 'required|url|ends_with:/', + 'sso_realm' => 'required', + 'sso_sip_identifier' => 'required' + ]); + + $space = Space::findOrFail($spaceId); + $space->sso_server_url = $request->get('sso_server_url'); + $space->sso_realm = $request->get('sso_realm'); + $space->sso_sip_identifier = $request->get('sso_sip_identifier'); + + if ($space->refreshSSOCertificate()) { + $space->save(); + } else { + return redirect()->back()->withErrors([ + 'sso_public_key' => __('The public key cannot be refreshed') + ]); + } + + return redirect()->route('admin.spaces.integration', $spaceId); + } +} diff --git a/flexiapi/app/Http/Middleware/AuthenticateJWT.php b/flexiapi/app/Http/Middleware/AuthenticateJWT.php index 4981061..fcd86bf 100644 --- a/flexiapi/app/Http/Middleware/AuthenticateJWT.php +++ b/flexiapi/app/Http/Middleware/AuthenticateJWT.php @@ -20,6 +20,7 @@ namespace App\Http\Middleware; use App\Account; +use App\Space; use Closure; use DateTimeImmutable; use Illuminate\Http\Request; @@ -39,13 +40,18 @@ class AuthenticateJWT { public function handle(Request $request, Closure $next) { - if ($request->bearerToken() && config('services.jwt.rsa_public_key_pem')) { + if ($request->bearerToken() && $request->space?->sso_public_key) { if (!extension_loaded('sodium')) { abort(403, "PHP Sodium extension isn't loaded"); } - $publicKey = InMemory::plainText(config('services.jwt.rsa_public_key_pem')); - $token = (new Parser(new JoseEncoder()))->parse($request->bearerToken()); + $publicKey = InMemory::plainText($request->space->sso_public_key); + + try { + $token = (new Parser(new JoseEncoder()))->parse($request->bearerToken()); + } catch (\Throwable $th) { + return $this->generateUnauthorizedBearerResponse($request->space, 'invalid_token', 'Invalid bearer ' . $th->getMessage()); + } $signer = null; @@ -64,19 +70,19 @@ class AuthenticateJWT } if ($signer == null) { - return $this->generateUnauthorizedBearerResponse('invalid_token', 'Unsupported RSA signature'); + return $this->generateUnauthorizedBearerResponse($request->space, 'invalid_token', 'Unsupported RSA signature'); } if (!(new Validator())->validate($token, new SignedWith($signer, $publicKey))) { - return $this->generateUnauthorizedBearerResponse('invalid_token', 'Invalid JWT token signature'); + return $this->generateUnauthorizedBearerResponse($request->space, 'invalid_token', 'Invalid JWT token signature'); } if ($token->isExpired(new DateTimeImmutable())) { - return $this->generateUnauthorizedBearerResponse('invalid_token', 'Expired JWT token'); + return $this->generateUnauthorizedBearerResponse($request->space, 'invalid_token', 'Expired JWT token'); } $account = null; - $identifierKey = config('services.jwt.sip_identifier'); + $identifierKey = $request->space->sso_sip_identifier; if ($identifierKey == '') $identifierKey = 'sip_identity'; if ($token->claims()->has($identifierKey)) { @@ -101,7 +107,7 @@ class AuthenticateJWT return $next($request); } - if (!empty(config('app.account_authentication_bearer')) + if ($request->space?->sso_authentication_bearer // Bypass the JWT auth if we have an API Key && !$request->header('x-api-key') && !$request->cookie('x-api-key') @@ -110,7 +116,7 @@ class AuthenticateJWT $response->header( 'WWW-Authenticate', - 'Bearer ' . config('app.account_authentication_bearer') + 'Bearer ' . $request->space?->sso_authentication_bearer ); $response->setStatusCode(401); @@ -121,10 +127,10 @@ class AuthenticateJWT return $next($request); } - private function generateUnauthorizedBearerResponse(string $error, string $description): Response + private function generateUnauthorizedBearerResponse(Space $space, string $error, string $description): Response { - $bearer = 'Bearer ' . config('app.account_authentication_bearer'); - $bearer .= !empty(config('app.account_authentication_bearer')) + $bearer = 'Bearer ' . $space->sso_authentication_bearer; + $bearer .= $space->sso_authentication_bearer != null ? ', ' : ''; diff --git a/flexiapi/app/Space.php b/flexiapi/app/Space.php index 7b57473..c6003e0 100644 --- a/flexiapi/app/Space.php +++ b/flexiapi/app/Space.php @@ -22,13 +22,16 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Http; use Carbon\Carbon; +use CoderCat\JWKToPEM\JWKConverter; class Space extends Model { use HasFactory; protected $with = ['emailServer', 'carddavServers']; + protected $fillable = ['host', 'sso_public_key', 'sso_server_url', 'sso_realm']; public const FORBIDDEN_KEYS = [ 'account_proxy_registrar_address', @@ -66,13 +69,18 @@ class Space extends Model 'super' => 'boolean', ]; + protected $attributes = [ + 'sso_sip_identifier' => 'sip_identity' + ]; + public const HOST_REGEX = '[\w\-]+'; public const DOMAIN_REGEX = '(?=^.{4,253}$)(^((?!-)[a-z0-9-]{1,63}(?superAdmin) { return; @@ -110,13 +118,13 @@ class Space extends Model public function scopeNotFull(Builder $query) { return $query->where('max_accounts', 0) - ->orWhereRaw('max_accounts > (select count(*) from accounts where domain = spaces.domain)'); + ->orWhereRaw('max_accounts > (select count(*) from accounts where domain = spaces.domain)'); } public function getAccountsPercentageAttribute(): int { if ($this->max_accounts != null) { - return (int)($this->accounts()->count() / $this->max_accounts * 100); + return (int) ($this->accounts()->count() / $this->max_accounts * 100); } return Command::SUCCESS; @@ -137,6 +145,37 @@ class Space extends Model return $this->host == config('app.root_host'); } + public function refreshSSOCertificate(): bool + { + if (isset($this->attributes['sso_server_url']) && isset($this->attributes['sso_realm'])) { + $response = Http::get($this->attributes['sso_server_url'] . '/realms/' . $this->attributes['sso_realm'] . '/protocol/openid-connect/certs'); + $jwkConverter = new JWKConverter(); + + if ($response->status() == '200' && $publicKey = $response->json('keys')[0]) { + $this->attributes['sso_public_key'] = $jwkConverter->toPEM($publicKey); + $this->attributes['updated_at'] = Carbon::now(); + + return true; + } + } + + return false; + } + + /** + * Non standard authentication flow based on RFC 8898 + */ + public function getSSOAuthenticationBearerAttribute(): ?string + { + if (isset($this->attributes['sso_server_url']) && isset($this->attributes['sso_realm'])) { + return + 'authz_server="' . $this->attributes['sso_server_url'] . 'realms/' . $this->attributes['sso_realm'] . '"' . + ',realm="' . $this->attributes['sso_realm'] . '"'; + } + + return null; + } + public function getAccountsPercentageClassAttribute(): string { if ($this->getAccountsPercentageAttribute() >= 80) { @@ -153,7 +192,7 @@ class Space extends Model public function getDaysLeftAttribute(): ?int { if ($this->expire_at != null) { - return (int)$this->expire_at->diffInDays(Carbon::now()) + 1; + return (int) $this->expire_at->diffInDays(Carbon::now()) + 1; } return null; diff --git a/flexiapi/composer.json b/flexiapi/composer.json index 47fec85..2a066bd 100644 --- a/flexiapi/composer.json +++ b/flexiapi/composer.json @@ -10,6 +10,7 @@ "require": { "php": ">=8.2", "awobaz/compoships": "^2.4.1", + "codercat/jwk-to-pem": "^1.1", "comcast/php-legal-licenses": "^2.2", "doctrine/dbal": "^3.10.1", "endroid/qr-code": "^5.1", diff --git a/flexiapi/composer.lock b/flexiapi/composer.lock index f018585..c3a4ce2 100644 --- a/flexiapi/composer.lock +++ b/flexiapi/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3e094d7231e491736d1b2cb70db958de", + "content-hash": "57ecd41850aee08a3926f70211252257", "packages": [ { "name": "awobaz/compoships", @@ -252,6 +252,50 @@ ], "time": "2023-12-11T17:09:12+00:00" }, + { + "name": "codercat/jwk-to-pem", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/acodercat/php-jwk-to-pem.git", + "reference": "4b3cdcf5f87b9b074f132f763a6b7b82c7d3ff1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/acodercat/php-jwk-to-pem/zipball/4b3cdcf5f87b9b074f132f763a6b7b82c7d3ff1d", + "reference": "4b3cdcf5f87b9b074f132f763a6b7b82c7d3ff1d", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "CoderCat\\JWKToPEM\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "codercat", + "email": "1067302838@qq.com" + } + ], + "description": "Convert JWK to PEM format.", + "support": { + "issues": "https://github.com/acodercat/php-jwk-to-pem/issues", + "source": "https://github.com/acodercat/php-jwk-to-pem/tree/1.1" + }, + "time": "2021-04-28T07:37:03+00:00" + }, { "name": "comcast/php-legal-licenses", "version": "v2.2.0", @@ -4128,6 +4172,125 @@ }, "time": "2025-01-02T16:09:40+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "parsedown/laravel", "version": "1.2.1", @@ -4383,6 +4546,116 @@ ], "time": "2025-12-27T19:41:33+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.52", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2026-04-27T07:02:15+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "10.1.16", diff --git a/flexiapi/config/app.php b/flexiapi/config/app.php index 50f1050..fec357a 100644 --- a/flexiapi/config/app.php +++ b/flexiapi/config/app.php @@ -24,7 +24,6 @@ return [ 'account_email_unique' => env('ACCOUNT_EMAIL_UNIQUE', false), 'account_username_regex' => env('ACCOUNT_USERNAME_REGEX', '^[a-z0-9+_.-]*$'), 'account_default_password_algorithm' => env('ACCOUNT_DEFAULT_PASSWORD_ALGORITHM', 'SHA-256'), - 'account_authentication_bearer' => env('ACCOUNT_AUTHENTICATION_BEARER', null), /** * Time limit before the API Key and related cookie are expired diff --git a/flexiapi/config/services.php b/flexiapi/config/services.php index d487efc..b851d5e 100644 --- a/flexiapi/config/services.php +++ b/flexiapi/config/services.php @@ -29,10 +29,4 @@ return [ 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], - - 'jwt' => [ - 'rsa_public_key_pem' => env('JWT_RSA_PUBLIC_KEY_PEM'), - 'sip_identifier' => env('JWT_SIP_IDENTIFIER', 'sip_identity'), - ], - ]; diff --git a/flexiapi/database/migrations/2026_04_21_140240_add_sso_configuration_to_spaces.php b/flexiapi/database/migrations/2026_04_21_140240_add_sso_configuration_to_spaces.php new file mode 100644 index 0000000..b3882fd --- /dev/null +++ b/flexiapi/database/migrations/2026_04_21_140240_add_sso_configuration_to_spaces.php @@ -0,0 +1,28 @@ +string('sso_server_url')->nullable(); + $table->string('sso_realm')->nullable(); + $table->string('sso_sip_identifier')->default('sip_identity'); + $table->text('sso_public_key')->nullable(); + }); + } + + public function down(): void + { + Schema::table('spaces', function (Blueprint $table) { + $table->dropColumn('sso_server_url'); + $table->dropColumn('sso_realm'); + $table->dropColumn('sso_sip_identifier'); + $table->dropColumn('sso_public_key'); + }); + } +}; diff --git a/flexiapi/lang/fr.json b/flexiapi/lang/fr.json index 3b718f1..db28c1c 100644 --- a/flexiapi/lang/fr.json +++ b/flexiapi/lang/fr.json @@ -140,8 +140,11 @@ "Integration": "Intégration", "Intercom features": "Fonctionnalités d'interphonie", "It might actually disable this page, be careful": "Cette page pourrait être désactivée, faites attention", + "JWT key containing the user's SIP identity. sip_identity by default.": "Clef du JWT contenant l'identité SIP de l'utilisateur. sip_identity par défaut.", "Key": "Clef", + "SSO Server": "Serveur SSO", "Last used": "Dernière utilisation", + "Last update": "Dernière mise à jour", "Leave empty to create a root Space.": "Laisser vide si vous souhaitez créer un Espace à la racine", "Limit the number of results": "Limiter le nomber de résultats", "List of vcard fields to match for SIP domain": "Liste des champs vcard à matcher pour le domaine SIP", @@ -192,10 +195,12 @@ "Provisioning": "Déploiement", "Proxy/registrar address":"Adresse Proxy/registrar", "Public registration": "Inscription publiques", + "Public key": "Clef publique", "QR Code scanning": "Scan de QR Code", "Realm": "Royaume", "Recover your account using your email": "Récupérer votre compte avec votre email", "Recorded at": "Enregistré le", + "Refresh": "Rafraichir", "Register": "Inscription", "Registrar": "Registrar", "Registration confirmed": "Confirmation de l'inscription", @@ -222,10 +227,12 @@ "Send an email to the user with provisioning information": "Envoyer un email à l'utilisateur avec les informations de déploiement", "Send": "Envoyer", "Separated by commas": "Séparé par des virgules", + "Server URL": "URL du serveur", "Settings": "Paramètres", "Show usernames only": "Afficher uniquement les noms d'utilisateur", "SIP Adress": "Adresse SIP", "SIP Domain": "Domaine SIP", + "SIP Identifier": "Identifiant SIP", "Space": "Espace", "Spaces": "Espaces", "Statistics": "Statistiques", @@ -244,6 +251,7 @@ "The first line contains the labels": "La premières ligne contient les étiquettes", "The following email address wants to register to the mailing list:":"L’adresse e-mail suivante souhaite s’inscrire à la liste de diffusion :", "The link can only be visited once": "Le lien ne peut être utilisé qu'une fois", + "The public key cannot be refreshed": "La clef publique ne peut être rafraichie", "Third-party SIP account": "Compte SIP tiers", "This code is valid for :minutes minutes.": "Ce code est valable pendant :minutes minutes.", "This link is not available anymore.": "Ce lien n'est plus disponible.", diff --git a/flexiapi/licenses.md b/flexiapi/licenses.md index 91ac889..b5ae467 100644 --- a/flexiapi/licenses.md +++ b/flexiapi/licenses.md @@ -112,6 +112,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +### codercat/jwk-to-pem (Version 1.1 | 4b3cdcf) +Convert JWK to PEM format. +Homepage: Not configured. +Licenses Used: MIT +MIT License + +Copyright (c) 2018 codercat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + ### comcast/php-legal-licenses (Version v2.2.0 | 2b01ea1) A utility to generate a Licenses file containing the full license text for every dependency in your project for legal purposes. Homepage: https://github.com/Comcast/php-legal-licenses @@ -2028,6 +2055,88 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +### paragonie/constant_time_encoding (Version v3.1.3 | d5b01a3) +Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16) +Homepage: Not configured. +Licenses Used: MIT +The MIT License (MIT) + +Copyright (c) 2016 - 2022 Paragon Initiative Enterprises + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------ +This library was based on the work of Steve "Sc00bz" Thomas. +------------------------------------------------------------------------------ + +The MIT License (MIT) + +Copyright (c) 2014 Steve Thomas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +### paragonie/random_compat (Version v9.99.100 | 996434e) +PHP 5.x polyfill for random_bytes() and random_int() from PHP 7 +Homepage: Not configured. +Licenses Used: MIT +The MIT License (MIT) + +Copyright (c) 2015 Paragon Initiative Enterprises + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + ### parsedown/laravel (Version 1.2.1 | c713ffe) Official Parsedown's Laravel Wrapper. Homepage: http://parsedown.org @@ -2333,6 +2442,31 @@ Licenses Used: Apache-2.0 See the License for the specific language governing permissions and limitations under the License. +### phpseclib/phpseclib (Version 3.0.52 | 2adaefc) +PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc. +Homepage: http://phpseclib.sourceforge.net +Licenses Used: MIT +Copyright (c) 2011-2019 TerraFrost and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ### phpunit/php-code-coverage (Version 10.1.16 | 7e30826) Library that provides collection, processing, and rendering functionality for PHP code coverage information. Homepage: https://github.com/sebastianbergmann/php-code-coverage @@ -2679,14 +2813,14 @@ Homepage: https://github.com/php-fig/http-client Licenses Used: MIT Copyright (c) 2017 PHP Framework Interoperability Group -Permission is hereby granted, free of charge, to any person obtaining a copy +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR @@ -2731,14 +2865,14 @@ Homepage: https://github.com/php-fig/http-message Licenses Used: MIT Copyright (c) 2014 PHP Framework Interoperability Group -Permission is hereby granted, free of charge, to any person obtaining a copy +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR @@ -2756,14 +2890,14 @@ Homepage: https://github.com/php-fig/log Licenses Used: MIT Copyright (c) 2012 PHP Framework Interoperability Group -Permission is hereby granted, free of charge, to any person obtaining a copy +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR diff --git a/flexiapi/resources/views/admin/space/integration.blade.php b/flexiapi/resources/views/admin/space/integration.blade.php index b8269d9..53ea431 100644 --- a/flexiapi/resources/views/admin/space/integration.blade.php +++ b/flexiapi/resources/views/admin/space/integration.blade.php @@ -10,17 +10,28 @@
+ @if ($space->emailServer) + {{ __('Edit') }} + {{ __('Delete') }} + @else + {{ __('Configure') }} + @endif

{{ __('Email Server') }}

@if ($space->emailServer) {{ $space->emailServer->host}}

@endif - @if ($space->emailServer) - {{ __('Edit') }} - {{ __('Delete') }} - @else - {{ __('Configure') }} +

+
+ +
+ {{ __('Configure') }} + +

{{ __('SSO Server') }}

+

+ @if ($space->sso_server_url) + {{ $space->sso_server_url}}

@endif

@@ -38,14 +49,13 @@
@foreach ($space->carddavServers as $carddavServer)
- {{ $carddavServer->accounts()->count() }} + {{ __('Edit') }} + {{ __('Delete') }}

{{ $carddavServer->name }}

+ {{ $carddavServer->accounts()->count() }} {{ $carddavServer->uri}}
-
- {{ __('Edit') }} - {{ __('Delete') }}

@endforeach diff --git a/flexiapi/resources/views/admin/space/sso_server/show.blade.php b/flexiapi/resources/views/admin/space/sso_server/show.blade.php new file mode 100644 index 0000000..e4cc8b2 --- /dev/null +++ b/flexiapi/resources/views/admin/space/sso_server/show.blade.php @@ -0,0 +1,57 @@ +@extends('layouts.main') + +@section('breadcrumb') + @include('admin.parts.breadcrumb.spaces.integration') + +@endsection + +@section('content') +
+

{{ $space->name }}

+
+ +
+ @csrf + @method('post') +
+ + + @include('parts.errors', ['name' => 'sso_server_url']) +
+
+ + + @include('parts.errors', ['name' => 'sso_realm']) +
+
+ + + @include('parts.errors', ['name' => 'sso_sip_identifier']) + {{ __("JWT key containing the user's SIP identity. sip_identity by default.")}} +
+
+ +
+ +
+ + @include('parts.errors', ['name' => 'sso_public_key']) + + @if ($space->sso_public_key) +

{{ __('Public key') }}

{{ __('Last update') }}: {{ $space->updated_at }} + +
+
{{ $space->sso_public_key }}
+
+ {{ __('Refresh') }} +
+ @endif + + + +@endsection diff --git a/flexiapi/routes/web.php b/flexiapi/routes/web.php index 579310a..881c38a 100644 --- a/flexiapi/routes/web.php +++ b/flexiapi/routes/web.php @@ -57,6 +57,7 @@ use App\Http\Controllers\Admin\Space\CardDavServerController; use App\Http\Controllers\Admin\Space\ContactsListContactController; use App\Http\Controllers\Admin\Space\ContactsListController; use App\Http\Controllers\Admin\Space\EmailServerController; +use App\Http\Controllers\Admin\Space\SSOServerController; use App\Http\Controllers\Admin\SpaceController; use App\Http\Controllers\Admin\StatisticsController; use Illuminate\Support\Facades\Route; @@ -88,7 +89,6 @@ Route::name('file.')->prefix('f')->controller(FileController::class)->group(func }); Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key']], function () { - Route::get('provisioning/me', [ProvisioningController::class, 'me'])->name('provisioning.me'); // vCard 4.0 @@ -199,6 +199,11 @@ Route::middleware(['feature.web_panel_enabled'])->group(function () { Route::get('delete', 'delete')->name('delete'); Route::delete('/', 'destroy')->name('destroy'); }); + Route::name('keycloak.')->prefix('{space}/keycloak')->controller(SSOServerController::class)->group(function () { + Route::get('/', 'show')->name('show'); + Route::get('refresh_public_key', 'refreshPublicKey')->name('refresh_public_key'); + Route::post('/', 'store')->name('store'); + }); Route::resource('{space}/carddavs', CardDavServerController::class, ['except' => ['index', 'show']]); Route::get('{space}/carddavs/{carddav}/delete', [CardDavServerController::class, 'delete'])->name('carddavs.delete'); diff --git a/flexiapi/tests/Feature/AccountJWTAuthenticationTest.php b/flexiapi/tests/Feature/AccountJWTAuthenticationTest.php index 2ae4622..d851fd0 100644 --- a/flexiapi/tests/Feature/AccountJWTAuthenticationTest.php +++ b/flexiapi/tests/Feature/AccountJWTAuthenticationTest.php @@ -44,7 +44,7 @@ class AccountJWTAuthenticationTest extends TestCase { parent::setUp(); - $keys = openssl_pkey_new(array("private_key_bits" => 4096,"private_key_type" => OPENSSL_KEYTYPE_RSA)); + $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); } @@ -52,15 +52,20 @@ class AccountJWTAuthenticationTest extends TestCase public function testBaseProvisioning() { # JWT is disabled if Sodium is not loaded - if (!extension_loaded('sodium')) return; + 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]); + $space = \App\Space::where('domain', $password->account->domain)->first(); + $space->update([ + 'host' => $domain, + 'sso_public_key' => $this->serverPublicKeyPem, + 'sso_sso_server_url' => 'https://sso.test/', + 'sso_realm' => 'sip.test.org' + ]); config()->set('app.sip_domain', $domain); - config()->set('services.jwt.rsa_public_key_pem', $this->serverPublicKeyPem); $this->get($this->route)->assertStatus(400); @@ -69,7 +74,7 @@ class AccountJWTAuthenticationTest extends TestCase $token = (new JwtFacade(null, $clock))->issue( new Sha256(), InMemory::plainText($this->serverPrivateKeyPem), - static fn ( + static fn( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('email', $password->account->email) @@ -79,13 +84,10 @@ class AccountJWTAuthenticationTest extends TestCase // 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 ( + static fn( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('sip_identity', 'sip:' . $password->account->username . '@' . $password->account->domain) @@ -93,13 +95,11 @@ class AccountJWTAuthenticationTest extends TestCase $this->checkToken($token); - // Handle JWT_SIP_IDENTIFIER= - config()->set('services.jwt.sip_identifier', ''); - + // Handle empty sso_sip_identifier $token = (new JwtFacade(null, $clock))->issue( new Sha256(), InMemory::plainText($this->serverPrivateKeyPem), - static fn ( + static fn( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('sip_identity', 'sip:' . $password->account->username . '@' . $password->account->domain) @@ -109,12 +109,12 @@ class AccountJWTAuthenticationTest extends TestCase // Custom SIP identifier $otherIdentifier = 'sip_other_identifier'; - config()->set('services.jwt.sip_identifier', $otherIdentifier); + \App\Space::where('domain', $password->account->domain)->update(['sso_sip_identifier' => 'sip_other_identifier']); $token = (new JwtFacade(null, $clock))->issue( new Sha256(), InMemory::plainText($this->serverPrivateKeyPem), - static fn ( + static fn( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim($otherIdentifier, 'sip:' . $password->account->username . '@' . $password->account->domain) @@ -126,7 +126,7 @@ class AccountJWTAuthenticationTest extends TestCase $token = (new JwtFacade(null, $clock))->issue( new Sha512(), InMemory::plainText($this->serverPrivateKeyPem), - static fn ( + static fn( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('email', $password->account->email) @@ -140,84 +140,89 @@ class AccountJWTAuthenticationTest extends TestCase $token = (new JwtFacade(null, $oldClock))->issue( new Sha256(), InMemory::plainText($this->serverPrivateKeyPem), - static fn ( + static fn( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('email', $password->account->email) ); $response = $this->withHeaders([ - 'Authorization' => 'Bearer ' . $token->toString(), - 'x-linphone-provisioning' => true, - ]) + '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, - ]) + 'Authorization' => 'Bearer ' . $token->toString(), + 'x-linphone-provisioning' => true, + ]) ->get($this->accountRoute) ->assertStatus(401); - $this->assertStringContainsString($bearer . ', ', $response->headers->get('WWW-Authenticate')); + $this->assertStringContainsString($space->sso_authentication_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 ( + static fn( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('email', 'unknow@man.org') ); $this->withHeaders([ - 'Authorization' => 'Bearer ' . $token->toString(), - 'x-linphone-provisioning' => true, - ]) + '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)); + $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 ( + static fn( Builder $builder, DateTimeImmutable $issuedAt ): Builder => $builder->withClaim('email', $password->account->email) ); $this->withHeaders([ - 'Authorization' => 'Bearer ' . $wrongToken->toString(), - 'x-linphone-provisioning' => true, - ]) + '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); + # JWT is disabled if Sodium is not loaded + if (!extension_loaded('sodium')) + return; - Password::factory()->create(); + $password = Password::factory()->create(); + $space = \App\Space::where('domain', $password->account->domain)->first(); + $space->update([ + 'sso_public_key' => $this->serverPublicKeyPem, + 'sso_server_url' => 'https://auth_bearer.com/', + 'sso_realm' => 'realm' + ]); $response = $this->json($this->method, $this->routeAccountMe) ->assertStatus(401); $this->assertStringContainsString( - 'Bearer ' . $value, + 'Bearer ' . $space->sso_authentication_bearer, $response->headers->all()['www-authenticate'][0] ); @@ -227,19 +232,19 @@ class AccountJWTAuthenticationTest extends TestCase ->assertStatus(401); $this->assertStringContainsString( - 'Bearer ' . $value, + 'Bearer ' . $space->sso_authentication_bearer, $response->headers->all()['www-authenticate'][0] ); // Wrong bearer message $this->withHeaders([ - 'Authorization' => 'Bearer 1234' - ]) + 'Authorization' => 'Bearer 1234' + ]) ->json($this->method, $this->routeAccountMe) ->assertStatus(401); $this->assertStringContainsString( - 'Bearer ' . $value, + 'Bearer ' . $space->sso_authentication_bearer, $response->headers->all()['www-authenticate'][0] ); } @@ -247,9 +252,9 @@ class AccountJWTAuthenticationTest extends TestCase protected function checkToken(UnencryptedToken $token): void { $this->withHeaders([ - 'Authorization' => 'Bearer ' . $token->toString(), - 'x-linphone-provisioning' => true, - ]) + 'Authorization' => 'Bearer ' . $token->toString(), + 'x-linphone-provisioning' => true, + ]) ->get($this->accountRoute) ->assertStatus(200) ->assertHeader('Content-Type', 'application/xml')