Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement JWT auth for signaling connections (hello v2) #7472

Merged
merged 6 commits into from
Aug 12, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/internal-signaling.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
| `stunservers` | array | v3 | STUN servers |
| `turnservers` | array | v3 | TURN servers |
| `sipDialinInfo` | string | v2 | Generic SIP dial-in information for this conversation (admin free text containing the phone number etc) |
| `helloAuthParams` | array | v3 | Parameters of the different `hello` versions for the external signaling server. |

- STUN server

Expand Down
5 changes: 5 additions & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,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;
fancycode marked this conversation as resolved.
Show resolved Hide resolved
}

if ($this->serverConfig->getAppValue('spreed', 'has_reference_id', 'no') === 'yes') {
$capabilities['features'][] = 'chat-reference-id';
}
Expand Down
124 changes: 123 additions & 1 deletion 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,10 +353,26 @@ public function getHideSignalingWarning(): bool {
}

/**
* @param int $version
* @param string $userId
* @return string
*/
public function getSignalingTicket(?string $userId): string {
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 {
Expand All @@ -369,6 +398,99 @@ 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) === 'ES') {
$privKey = openssl_pkey_new([
'curve_name' => 'prime256v1',
'private_key_type' => OPENSSL_KEYTYPE_EC,
]);
$pubKey = openssl_pkey_get_details($privKey);
$public = $pubKey['key'];
if (!openssl_pkey_export($privKey, $secret)) {
throw new \Exception('Could not export private key');
}
} elseif (substr($alg, 0, 2) === 'RS') {
$privKey = openssl_pkey_new([
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
$pubKey = openssl_pkey_get_details($privKey);
$public = $pubKey['key'];
if (!openssl_pkey_export($privKey, $secret)) {
throw new \Exception('Could not export private key');
}
} elseif ($alg === 'EdDSA') {
$privKey = sodium_crypto_sign_keypair();
$public = base64_encode(sodium_crypto_sign_publickey($privKey));
$secret = base64_encode(sodium_crypto_sign_secretkey($privKey));
} else {
throw new \Exception('Unsupported algorithm ' . $alg);
}

$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
20 changes: 16 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,22 @@ public function getSettings(string $token = ''): DataResponse {
$signalingMode = $this->talkConfig->getSignalingMode();
$signaling = $this->signalingManager->getSignalingServerLinkForConversation($room);

$helloAuthParams = [
'1.0' => [
'userid' => $this->userId,
'ticket' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V1, $this->userId),
],
'2.0' => [
'token' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V2, $this->userId),
],
];
$data = [
'signalingMode' => $signalingMode,
'userId' => $this->userId,
'hideWarning' => $signaling !== '' || $this->talkConfig->getHideSignalingWarning(),
'server' => $signaling,
'ticket' => $this->talkConfig->getSignalingTicket($this->userId),
'ticket' => $helloAuthParams['1.0']['ticket'],
'helloAuthParams' => $helloAuthParams,
'stunservers' => $stun,
'turnservers' => $turn,
'sipDialinInfo' => $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : '',
Expand Down Expand Up @@ -540,9 +554,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
3 changes: 2 additions & 1 deletion lib/Signaling/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public function isCompatibleSignalingServer(IResponse $response): bool {
$features = explode(',', $featureHeader);
$features = array_map('trim', $features);
return in_array('audio-video-permissions', $features, true)
&& in_array('incall-all', $features, true);
&& in_array('incall-all', $features, true)
&& in_array('hello-v2', $features, true);
}

public function getSignalingServerLinkForConversation(?Room $room): string {
Expand Down
58 changes: 50 additions & 8 deletions src/utils/signaling.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ function Standalone(settings, urls) {
url = url.slice(0, -1)
}
this.url = url + '/spreed'
this.welcomeTimeoutMs = 3000
this.initialReconnectIntervalMs = 1000
this.maxReconnectIntervalMs = 16000
this.reconnectIntervalMs = this.initialReconnectIntervalMs
Expand Down Expand Up @@ -658,7 +659,11 @@ Signaling.Standalone.prototype.connect = function() {
this.signalingConnectionWarning = null
}
this.reconnectIntervalMs = this.initialReconnectIntervalMs
this.sendHello()
if (this.settings.helloAuthParams['2.0']) {
this.waitForWelcomeTimeout = setTimeout(this.welcomeTimeout.bind(this), this.welcomeTimeoutMs)
} else {
this.sendHello()
}
}.bind(this)
this.socket.onerror = function(event) {
console.error('Error', event)
Expand Down Expand Up @@ -713,6 +718,9 @@ Signaling.Standalone.prototype.connect = function() {
this._trigger('onBeforeReceiveMessage', [data])
const message = {}
switch (data.type) {
case 'welcome':
this.welcomeReceived(data)
break
case 'hello':
if (!id) {
// Only process if not received as result of our "hello".
Expand Down Expand Up @@ -744,10 +752,17 @@ Signaling.Standalone.prototype.connect = function() {
this._trigger('message', [message])
break
case 'error':
if (data.error.code === 'processing_failed') {
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')
} else {
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 All @@ -760,6 +775,30 @@ Signaling.Standalone.prototype.connect = function() {
}.bind(this)
}

Signaling.Standalone.prototype.welcomeReceived = function(data) {
console.debug('Welcome received', data)
if (this.waitForWelcomeTimeout !== null) {
clearTimeout(this.waitForWelcomeTimeout)
this.waitForWelcomeTimeout = null
}

this.features = {}
let i
if (data.welcome && data.welcome.features) {
const features = data.welcome.features
for (i = 0; i < features.length; i++) {
this.features[features[i]] = true
}
}

this.sendHello()
}

Signaling.Standalone.prototype.welcomeTimeout = function() {
console.warn('No welcome received, assuming old-style signaling server')
this.sendHello()
}

Signaling.Standalone.prototype.sendBye = function() {
if (this.connected) {
this.doSend({
Expand Down Expand Up @@ -901,16 +940,19 @@ Signaling.Standalone.prototype.sendHello = function() {
// Already reconnected with a new session.
this._forceReconnect = false
const url = generateOcsUrl('apps/spreed/api/v3/signaling/backend')
let helloVersion
if (this.hasFeature('hello-v2') && this.settings.helloAuthParams['2.0']) {
helloVersion = '2.0'
} else {
helloVersion = '1.0'
}
msg = {
type: 'hello',
hello: {
version: '1.0',
version: helloVersion,
auth: {
url,
params: {
userid: this.settings.userId,
ticket: this.settings.ticket,
},
params: this.settings.helloAuthParams[helloVersion],
},
},
}
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
Loading