Fix FLEXIAPI-464 Add per Space custom RFC8898 flow

This commit is contained in:
Timothée Jaussoin 2026-04-30 14:09:10 +00:00
parent 8585095d99
commit 0669b0d965
16 changed files with 723 additions and 105 deletions

View file

@ -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

View file

@ -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 <value>) 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

View file

@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Admin\Space;
use App\Space;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class SSOServerController extends Controller
{
public function show(int $spaceId)
{
return view('admin.space.sso_server.show', [
'space' => 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);
}
}

View file

@ -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
? ', '
: '';

View file

@ -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}(?<!-)\.)+[a-z]{2,63}$)';
protected static function booted()
{
static::addGlobalScope('domain', function (Builder $builder) {
if (!Auth::hasUser()) return;
if (!Auth::hasUser())
return;
if (Auth::hasUser() || Auth::user()->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;

View file

@ -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",

275
flexiapi/composer.lock generated
View file

@ -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",

View file

@ -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

View file

@ -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'),
],
];

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('spaces', function (Blueprint $table) {
$table->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');
});
}
};

View file

@ -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:":"Ladresse e-mail suivante souhaite sinscrire à 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.",

View file

@ -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

View file

@ -10,17 +10,28 @@
<div class="grid third">
<div class="card">
@if ($space->emailServer)
<a class="btn small oppose" href="{{ route('admin.spaces.email.show', $space) }}">{{ __('Edit') }}</a>
<a class="btn small oppose tertiary" href="{{ route('admin.spaces.email.delete', $space) }}">{{ __('Delete') }}</a>
@else
<a class="btn small oppose secondary" href="{{ route('admin.spaces.email.show', $space) }}">{{ __('Configure') }}</a>
@endif
<span class="icon"><i class="ph ph-envelope"></i></span>
<h3>{{ __('Email Server') }}</h3>
<p>
@if ($space->emailServer)
{{ $space->emailServer->host}}<br /><br />
@endif
@if ($space->emailServer)
<a class="btn oppose" href="{{ route('admin.spaces.email.show', $space) }}">{{ __('Edit') }}</a>
<a class="btn oppose tertiary" href="{{ route('admin.spaces.email.delete', $space) }}">{{ __('Delete') }}</a>
@else
<a class="btn oppose secondary" href="{{ route('admin.spaces.email.show', $space) }}">{{ __('Configure') }}</a>
</p>
</div>
<div class="card">
<a class="btn small oppose secondary" href="{{ route('admin.spaces.keycloak.show', $space) }}">{{ __('Configure') }}</a>
<span class="icon"><i class="ph ph-key"></i></span>
<h3>{{ __('SSO Server') }}</h3>
<p>
@if ($space->sso_server_url)
<code>{{ $space->sso_server_url}}</code><br /><br />
@endif
</p>
</div>
@ -38,14 +49,13 @@
<div class="grid third">
@foreach ($space->carddavServers as $carddavServer)
<div class="card">
<small class="oppose"><i class="ph ph-users"></i> {{ $carddavServer->accounts()->count() }}</small>
<a class="btn small oppose" href="{{ route('admin.spaces.carddavs.edit', [$space, $carddavServer]) }}">{{ __('Edit') }}</a>
<a class="btn small oppose tertiary" href="{{ route('admin.spaces.carddavs.delete', [$space, $carddavServer]) }}">{{ __('Delete') }}</a>
<span class="icon"><i class="ph ph-identification-card"></i></span>
<h3>{{ $carddavServer->name }}</h3>
<p>
<small class="oppose"><i class="ph ph-users"></i> {{ $carddavServer->accounts()->count() }}</small>
{{ $carddavServer->uri}}<br />
<br />
<a class="btn oppose" href="{{ route('admin.spaces.carddavs.edit', [$space, $carddavServer]) }}">{{ __('Edit') }}</a>
<a class="btn oppose tertiary" href="{{ route('admin.spaces.carddavs.delete', [$space, $carddavServer]) }}">{{ __('Delete') }}</a>
</p>
</div>
@endforeach

View file

@ -0,0 +1,57 @@
@extends('layouts.main')
@section('breadcrumb')
@include('admin.parts.breadcrumb.spaces.integration')
<li class="breadcrumb-item active" aria-current="page">{{ __('SSO Server') }}</li>
@endsection
@section('content')
<header>
<h1><i class="ph ph-key"></i> {{ $space->name }}</h1>
</header>
<form method="POST"
action="{{ route('admin.spaces.keycloak.store', $space->id) }}"
id="show" accept-charset="UTF-8">
@csrf
@method('post')
<div>
<input placeholder="https://keycloak.server.tld/" required="required" name="sso_server_url" type="url"
value="@if($space->id){{ $space->sso_server_url }}@else{{ old('sso_server_url') }}@endif">
<label for="sso_server_url">{{ __('Server URL') }}</label>
@include('parts.errors', ['name' => 'sso_server_url'])
</div>
<div>
<input placeholder="cogip" required="required" name="sso_realm" type="text"
value="@if($space->id){{ $space->sso_realm }}@else{{ old('sso_realm') }}@endif">
<label for="sso_realm">{{ __('Realm') }}</label>
@include('parts.errors', ['name' => 'sso_realm'])
</div>
<div>
<input placeholder="sip_identity" name="sso_sip_identifier" type="text" required="required"
value="@if($space->id && isset($space->sso_sip_identifier)){{ $space->sso_sip_identifier }}@else{{ old('sso_sip_identifier') }}@endif">
<label for="sso_sip_identifier">{{ __('SIP Identifier') }}</label>
@include('parts.errors', ['name' => 'sso_sip_identifier'])
<span class="supporting">{{ __("JWT key containing the user's SIP identity. sip_identity by default.")}}</span>
</div>
</form>
<br />
<hr />
@include('parts.errors', ['name' => 'sso_public_key'])
@if ($space->sso_public_key)
<h4>{{ __('Public key') }}</h4> <small>{{ __('Last update') }}: {{ $space->updated_at }}</small>
<br />
<pre style="display: inline-block;"><code>{{ $space->sso_public_key }}</code></pre>
<br />
<a class="btn small secondary" href="{{ route('admin.spaces.keycloak.refresh_public_key', $space) }}">{{ __('Refresh') }}</a>
<hr />
@endif
<input form="show" class="btn" type="submit" value="@if($space->id){{ __('Update') }}@else{{ __('Create') }}@endif">
@endsection

View file

@ -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');

View file

@ -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')