Skip to content

Commit

Permalink
Implement JWT auth for signaling connection.
Browse files Browse the repository at this point in the history
Signed-off-by: Joachim Bauch <[email protected]>
  • Loading branch information
fancycode committed Jun 14, 2022
1 parent 3ea410d commit f38205a
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 19 deletions.
5 changes: 5 additions & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ public function getCapabilities(): array {

$capabilities['config']['conversations']['can-create'] = $user instanceof IUser && !$this->talkConfig->isNotAllowedToCreateConversations($user);

$pubKey = $this->talkConfig->getSignalingTokenPublicKey();
if ($pubKey) {
$capabilities['config']['signaling']['hello-v2-token-key'] = $pubKey;
}

if ($this->serverConfig->getAppValue('spreed', 'has_reference_id', 'no') === 'yes') {
$capabilities['features'][] = 'chat-reference-id';
}
Expand Down
118 changes: 116 additions & 2 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,31 @@

namespace OCA\Talk;

use Firebase\JWT\JWT;

use OCP\AppFramework\Utility\ITimeFactory;
use OCA\Talk\Events\GetTurnServersEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;

class Config {
public const SIGNALING_INTERNAL = 'internal';
public const SIGNALING_EXTERNAL = 'external';
public const SIGNALING_CLUSTER_CONVERSATION = 'conversation_cluster';

public const SIGNALING_TICKET_V1 = 1;
public const SIGNALING_TICKET_V2 = 2;

protected IConfig $config;
protected ITimeFactory $timeFactory;
private IGroupManager $groupManager;
private IUserManager $userManager;
private IURLGenerator $urlGenerator;
private ISecureRandom $secureRandom;
private IEventDispatcher $dispatcher;

Expand All @@ -47,11 +56,15 @@ class Config {
public function __construct(IConfig $config,
ISecureRandom $secureRandom,
IGroupManager $groupManager,
IUserManager $userManager,
IURLGenerator $urlGenerator,
ITimeFactory $timeFactory,
IEventDispatcher $dispatcher) {
$this->config = $config;
$this->secureRandom = $secureRandom;
$this->groupManager = $groupManager;
$this->userManager = $userManager;
$this->urlGenerator = $urlGenerator;
$this->timeFactory = $timeFactory;
$this->dispatcher = $dispatcher;
}
Expand Down Expand Up @@ -340,11 +353,27 @@ public function getHideSignalingWarning(): bool {
}

/**
* @param int $version
* @param string $userId
* @return string
*/
public function getSignalingTicket(?string $userId): string {
if (empty($userId)) {
public function getSignalingTicket(int $version, ?string $userId): string {
switch ($version) {
case self::SIGNALING_TICKET_V1:
return $this->getSignalingTicketV1($userId);
case self::SIGNALING_TICKET_V2:
return $this->getSignalingTicketV2($userId);
default:
return $this->getSignalingTicketV1($userId);
}
}

/**
* @param string $userId
* @return string
*/
private function getSignalingTicketV1(?string $userId): string {
if (empty($userId)) {
$secret = $this->config->getAppValue('spreed', 'signaling_ticket_secret');
} else {
$secret = $this->config->getUserValue($userId, 'spreed', 'signaling_ticket_secret');
Expand All @@ -369,6 +398,91 @@ public function getSignalingTicket(?string $userId): string {
return $data . ':' . $hash;
}

private function ensureSignalingTokenKeys(string $alg): void {
$secret = $this->config->getAppValue('spreed', 'signaling_token_privkey_' . strtolower($alg));
if ($secret) {
return;
}

if (substr($alg, 0, 2) === 'EC') {
$privKey = openssl_pkey_new([
'curve_name' => 'prime256v1',
'private_key_type' => OPENSSL_KEYTYPE_EC,
]);
} else if (substr($alg, 0, 2) === 'RS') {
$privKey = openssl_pkey_new([
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
} else {
throw new \Exception('Unsupported algorithm ' . $alg);
}
$pubKey = openssl_pkey_get_details($privKey);
$public = $pubKey['key'];

if (!openssl_pkey_export($privKey, $secret)) {
throw new \Exception('Could not export private key');
}

$this->config->setAppValue('spreed', 'signaling_token_privkey_' . strtolower($alg), $secret);
$this->config->setAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg), $public);
}

public function getSignalingTokenAlgorithm(): string {
return $this->config->getAppValue('spreed', 'signaling_token_alg', 'ES256');;
}

public function getSignalingTokenPrivateKey(?string $alg = null): string {
if (!$alg) {
$alg = $this->getSignalingTokenAlgorithm();
}
$this->ensureSignalingTokenKeys($alg);

return $this->config->getAppValue('spreed', 'signaling_token_privkey_' . strtolower($alg));
}

public function getSignalingTokenPublicKey(?string $alg = null): string {
if (!$alg) {
$alg = $this->getSignalingTokenAlgorithm();
}
$this->ensureSignalingTokenKeys($alg);

return $this->config->getAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg));
}

/**
* @param IUser $user
* @return array
*/
public function getSignalingUserData(IUser $user): array {
return [
'displayname' => $user->getDisplayName(),
];
}

/**
* @param string $userId
* @return string
*/
private function getSignalingTicketV2(?string $userId): string {
$timestamp = $this->timeFactory->getTime();
$data = [
'iss' => $this->urlGenerator->getAbsoluteURL(''),
'iat' => $timestamp,
'exp' => $timestamp + 60, // Valid for 1 minute.
];
$user = !empty($userId) ? $this->userManager->get($userId) : null;
if ($user instanceof IUser) {
$data['sub'] = $user->getUID();
$data['userdata'] = $this->getSignalingUserData($user);
}

$alg = $this->getSignalingTokenAlgorithm();
$secret = $this->getSignalingTokenPrivateKey($alg);
$token = JWT::encode($data, $secret, $alg);
return $token;
}

/**
* @param string $userId
* @param string $ticket
Expand Down
24 changes: 20 additions & 4 deletions lib/Controller/SignalingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
use OCP\DB\Exception;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\IUser;
Expand All @@ -59,6 +60,7 @@ class SignalingController extends OCSController {

public const EVENT_BACKEND_SIGNALING_ROOMS = self::class . '::signalingBackendRoom';

private IConfig $serverConfig;
private Config $talkConfig;
private \OCA\Talk\Signaling\Manager $signalingManager;
private TalkSession $session;
Expand All @@ -76,6 +78,7 @@ class SignalingController extends OCSController {

public function __construct(string $appName,
IRequest $request,
IConfig $serverConfig,
Config $talkConfig,
\OCA\Talk\Signaling\Manager $signalingManager,
TalkSession $session,
Expand All @@ -91,6 +94,7 @@ public function __construct(string $appName,
LoggerInterface $logger,
?string $UserId) {
parent::__construct($appName, $request);
$this->serverConfig = $serverConfig;
$this->talkConfig = $talkConfig;
$this->signalingManager = $signalingManager;
$this->session = $session;
Expand Down Expand Up @@ -157,12 +161,26 @@ public function getSettings(string $token = ''): DataResponse {
$signalingMode = $this->talkConfig->getSignalingMode();
$signaling = $this->signalingManager->getSignalingServerLinkForConversation($room);

// TODO: Autodetect and use if signaling server has feature flag "hello-v2".
if ($this->serverConfig->getAppValue('spreed', 'use-hello-v2', false)) {
$helloVersion = '2.0';
$helloAuthParams = [
'token' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V2, $this->userId),
];
} else {
$helloVersion = '1.0';
$helloAuthParams = [
'userid' => $this->userId,
'ticket' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V1, $this->userId),
];
}
$data = [
'signalingMode' => $signalingMode,
'userId' => $this->userId,
'hideWarning' => $signaling !== '' || $this->talkConfig->getHideSignalingWarning(),
'server' => $signaling,
'ticket' => $this->talkConfig->getSignalingTicket($this->userId),
'helloVersion' => $helloVersion,
'helloAuthParams' => $helloAuthParams,
'stunservers' => $stun,
'turnservers' => $turn,
'sipDialinInfo' => $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : '',
Expand Down Expand Up @@ -540,9 +558,7 @@ private function backendAuth(array $auth): DataResponse {
];
if (!empty($userId)) {
$response['auth']['userid'] = $user->getUID();
$response['auth']['user'] = [
'displayname' => $user->getDisplayName(),
];
$response['auth']['user'] = $this->talkConfig->getSignalingUserData($user);
}
$this->logger->debug('Validated signaling ticket for {user}', [
'user' => !empty($userId) ? $userId : '(guests)',
Expand Down
22 changes: 13 additions & 9 deletions src/utils/signaling.js
Original file line number Diff line number Diff line change
Expand Up @@ -744,10 +744,17 @@ Signaling.Standalone.prototype.connect = function() {
this._trigger('message', [message])
break
case 'error':
if (data.error.code === 'processing_failed') {
console.error('An error occurred processing the signaling message, please ask your server administrator to check the log file')
} else {
console.error('Ignore unknown error', data)
switch (data.error.code) {
case 'processing_failed':
console.error('An error occurred processing the signaling message, please ask your server administrator to check the log file')
break
case 'token_expired':
console.info('The signaling token is expired, need to update settings')
this._trigger('updateSettings')
break
default:
console.error('Ignore unknown error', data)
break
}
break
default:
Expand Down Expand Up @@ -904,13 +911,10 @@ Signaling.Standalone.prototype.sendHello = function() {
msg = {
type: 'hello',
hello: {
version: '1.0',
version: this.settings.helloVersion,
auth: {
url,
params: {
userid: this.settings.userId,
ticket: this.settings.ticket,
},
params: this.settings.helloAuthParams,
},
},
}
Expand Down
6 changes: 6 additions & 0 deletions src/utils/webrtc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ async function connectSignaling(token) {

if (!signaling) {
signaling = Signaling.createConnection(settings)
signaling.on('updateSettings', async function() {
const settings = await getSignalingSettings(token)
console.debug('Received updated settings', settings)
signaling.settings = settings
})

}

tokensInSignaling[token] = true
Expand Down
8 changes: 4 additions & 4 deletions tests/php/Controller/SignalingControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ public function testBackendAuth() {
'auth' => [
'params' => [
'userid' => 'invalid-userid',
'ticket' => $this->config->getSignalingTicket($this->userId),
'ticket' => $this->config->getSignalingTicket(Config::SIGNALING_TICKET_V1, $this->userId),
],
],
]);
Expand All @@ -291,7 +291,7 @@ public function testBackendAuth() {
'auth' => [
'params' => [
'userid' => 'unknown-userid',
'ticket' => $this->config->getSignalingTicket('unknown-userid'),
'ticket' => $this->config->getSignalingTicket(Config::SIGNALING_TICKET_V1, 'unknown-userid'),
],
],
]);
Expand Down Expand Up @@ -320,7 +320,7 @@ public function testBackendAuth() {
'auth' => [
'params' => [
'userid' => $this->userId,
'ticket' => $this->config->getSignalingTicket($this->userId),
'ticket' => $this->config->getSignalingTicket(Config::SIGNALING_TICKET_V1, $this->userId),
],
],
]);
Expand All @@ -341,7 +341,7 @@ public function testBackendAuth() {
'auth' => [
'params' => [
'userid' => '',
'ticket' => $this->config->getSignalingTicket(''),
'ticket' => $this->config->getSignalingTicket(Config::SIGNALING_TICKET_V1, ''),
],
],
]);
Expand Down

0 comments on commit f38205a

Please sign in to comment.