[GH-ISSUE #601] Setting Phone number via API #121

Open
opened 2026-02-26 05:33:23 +03:00 by kerem · 2 comments
Owner

Originally created by @ogamingSCV on GitHub (Dec 14, 2023).
Original GitHub issue: https://github.com/nextcloud/twofactor_gateway/issues/601

Hello,

I am currently exploring the use of the SMS 2FA plugin for Nextcloud and was wondering if there is a method to programmatically set the phone number for SMS 2FA through an API. I am particularly interested in automating this process for user management.

Any insights or help on this matter would be greatly appreciated.

Best regards,

Originally created by @ogamingSCV on GitHub (Dec 14, 2023). Original GitHub issue: https://github.com/nextcloud/twofactor_gateway/issues/601 Hello, I am currently exploring the use of the SMS 2FA plugin for Nextcloud and was wondering if there is a method to programmatically set the phone number for SMS 2FA through an API. I am particularly interested in automating this process for user management. Any insights or help on this matter would be greatly appreciated. Best regards,
Author
Owner

@oleua commented on GitHub (Dec 29, 2023):

Well, I use Signal as a 2FA and have patched lib/Service/Gateway/Signal/Gateway.php in order to pass the phone number as the account id, so the variable SIGNAL_CLI_DBUS_REST_API_ACCOUNT=>+XXXYYYYYYYYY is set in config.php

        /**
         * @param IUser $user
         * @param string $identifier
         * @param string $message
         *
         * @throws SmsTransmissionException
         * 2023-06-18 patched by OS: added the system variable of the registered number/account SIGNAL_CLI_DBUS_REST_API_ACCOUNT, modified send function
         */

        public function send(IUser $user, string $identifier, string $message) {
                $client = $this->clientService->newClient();
                // determine type of gateway
                $response = $client->get($this->config->getUrl() . '/v1/about');
                if ($response->getStatusCode() === 200) {
                // new v2 style gateway https://github.com/bbernhard/signal-cli-rest-api
                        $recipient_acct = [$identifier];
                        $registered_acct = $this->config2->getSystemValue('SIGNAL_CLI_DBUS_REST_API_ACCOUNT','');
                        $response = $client->post(
                                $this->config->getUrl() . '/v2/send',
                                [
                                        'json' => [
                                                'text_mode' => 'styled',
                                                'message' => $message,
                                                'number' => $registered_acct,
                                                'recipients' => $recipient_acct
                                                 ],

                                ]
                        );
                        $body = $response->getBody();
                        $json = json_decode($body, true);
                        if ($response->getStatusCode() !== 201 || is_null($json) || !is_array($json) || !isset($json['timestamp'])) {
                                $status = $response->getStatusCode();
                                throw new SmsTransmissionException("error reported by Signal gateway, status=$status, body=$body}");
                        }
                }
                else {
                        // Try old deprecated gateway https://gitlab.com/morph027/signal-web-gateway
                        $response = $client->post(
                                $this->config->getUrl() . '/v1/send/' . $identifier,
                                [
                                        'body' => [
                                                'to' => $identifier,
                                                'message' => $message,
                                        ],
                                        'json' => [ 'message' => $message ],
                                ]
                        );
                        $body = $response->getBody();
                        $json = json_decode($body, true);

                        if ($response->getStatusCode() !== 200 || is_null($json) || !is_array($json) || !isset($json['success']) || $json['success'] !== true) {
                                $status = $response->getStatusCode();
                                throw new SmsTransmissionException("error reported by Signal gateway, status=$status, body=$body}");
                        }
                }

        }

You may reuse it somehow. Ideally, it might be great to add this configuration to webUI.

<!-- gh-comment-id:1872339890 --> @oleua commented on GitHub (Dec 29, 2023): Well, I use Signal as a 2FA and have patched `lib/Service/Gateway/Signal/Gateway.php` in order to pass the phone number as the account id, so the variable `SIGNAL_CLI_DBUS_REST_API_ACCOUNT=>+XXXYYYYYYYYY` is set in `config.php` ``` /** * @param IUser $user * @param string $identifier * @param string $message * * @throws SmsTransmissionException * 2023-06-18 patched by OS: added the system variable of the registered number/account SIGNAL_CLI_DBUS_REST_API_ACCOUNT, modified send function */ public function send(IUser $user, string $identifier, string $message) { $client = $this->clientService->newClient(); // determine type of gateway $response = $client->get($this->config->getUrl() . '/v1/about'); if ($response->getStatusCode() === 200) { // new v2 style gateway https://github.com/bbernhard/signal-cli-rest-api $recipient_acct = [$identifier]; $registered_acct = $this->config2->getSystemValue('SIGNAL_CLI_DBUS_REST_API_ACCOUNT',''); $response = $client->post( $this->config->getUrl() . '/v2/send', [ 'json' => [ 'text_mode' => 'styled', 'message' => $message, 'number' => $registered_acct, 'recipients' => $recipient_acct ], ] ); $body = $response->getBody(); $json = json_decode($body, true); if ($response->getStatusCode() !== 201 || is_null($json) || !is_array($json) || !isset($json['timestamp'])) { $status = $response->getStatusCode(); throw new SmsTransmissionException("error reported by Signal gateway, status=$status, body=$body}"); } } else { // Try old deprecated gateway https://gitlab.com/morph027/signal-web-gateway $response = $client->post( $this->config->getUrl() . '/v1/send/' . $identifier, [ 'body' => [ 'to' => $identifier, 'message' => $message, ], 'json' => [ 'message' => $message ], ] ); $body = $response->getBody(); $json = json_decode($body, true); if ($response->getStatusCode() !== 200 || is_null($json) || !is_array($json) || !isset($json['success']) || $json['success'] !== true) { $status = $response->getStatusCode(); throw new SmsTransmissionException("error reported by Signal gateway, status=$status, body=$body}"); } } } ``` You may reuse it somehow. Ideally, it might be great to add this configuration to webUI.
Author
Owner

@oleua commented on GitHub (Jan 28, 2026):

In the v32 I do not use anymore the SIGNAL_CLI_DBUS_REST_API_ACCOUNT variable in config.php. I have adapted the configuration to make it compatible with bbernhard/signal-cli-rest-api, which I use:

In /apps/twofactor_gateway/lib/Provider/Channel/Signal/Gateway.php:

<?php

// modified by me

declare(strict_types=1);

/**
 * SPDX-FileCopyrightText: 2024 Christoph Wurst <christoph@winzerhof-wurst.at>
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

namespace OCA\TwoFactorGateway\Provider\Channel\Signal;

use OCA\TwoFactorGateway\Exception\MessageTransmissionException;
use OCA\TwoFactorGateway\Provider\FieldDefinition;
use OCA\TwoFactorGateway\Provider\Gateway\AGateway;
use OCA\TwoFactorGateway\Provider\Settings;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Http\Client\IClientService;
use OCP\IAppConfig;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;

/**
 * An integration of https://github.com/bbernhard/signal-cli-rest-api
 * with backward compatibility.
 * @method string getUrl()
 * @method AGateway setUrl(string $url)
 * @method string getAccount()
 * @method AGateway setAccount(string $account)
 */
class Gateway extends AGateway {
    public const ACCOUNT_UNNECESSARY = 'unnecessary';

    public function __construct(
        public IAppConfig $appConfig,
        private IClientService $clientService,
        private ITimeFactory $timeFactory,
        private LoggerInterface $logger,
    ) {
        parent::__construct($appConfig);
    }

    #[\Override]
    public function createSettings(): Settings {
        return new Settings(
            name: 'Signal',
            instructions: 'The gateway can send authentication codes to your Signal app.',
            fields: [
                new FieldDefinition(
                    field: 'url',
                    prompt: 'Please enter the URL of the Signal gateway:',
                    default: 'http://localhost:9922',
                ),
                new FieldDefinition(
                    field: 'account',
                    prompt: 'Please enter the registered Signal number (e.g. +380...):',
                ),
            ]
        );
    }

    #[\Override]
    public function send(string $identifier, string $message, array $extra = []): void {
        $client = $this->clientService->newClient();
        $url = rtrim($this->getUrl(), '/');
        $account = $this->getAccount();

        // 1. Identifying the gateway vial /v1/about
        try {
            $aboutResponse = $client->get($url . '/v1/about');
            if ($aboutResponse->getStatusCode() === 200) {
                $body = $aboutResponse->getBody();
                $jsonAbout = json_decode($body, true);
                $versions = $jsonAbout['versions'] ?? [];

                // Якщо це bbernhard/signal-cli-rest-api (підтримує v2)
                if (is_array($versions) && in_array('v2', $versions)) {
                    $payload = [
                        'text_mode' => 'styled',
                        'message' => $message,
                        'recipients' => [$identifier],
                    ];
                    
                    if ($account !== self::ACCOUNT_UNNECESSARY && !empty($account)) {
                        $payload['number'] = $account;
                    }

                    $response = $client->post($url . '/v2/send', ['json' => $payload]);
                    $this->validateV2Response($response);
                    return;
                }
            }
        } catch (\Exception $e) {
            $this->logger->error("Signal Gateway check failed: " . $e->getMessage());
        }

        // 2. If v2 hasn't worked use JSON RPC (new standard since NC32)
        $rpcResponse = $client->post($url . '/api/v1/rpc', [
            'http_errors' => false,
            'json' => [
                'jsonrpc' => '2.0',
                'method' => 'version',
                'id' => 'v_' . $this->timeFactory->getTime(),
            ],
        ]);

        if (in_array($rpcResponse->getStatusCode(), [200, 201])) {
            $params = [
                'message' => $message,
                'recipient' => $identifier,
                'account' => $account,
            ];
            $response = $client->post($url . '/api/v1/rpc', [
                'json' => [
                    'jsonrpc' => '2.0',
                    'method' => 'send',
                    'id' => 'msg_' . $this->timeFactory->getTime(),
                    'params' => $params,
                ],
            ]);
            $this->validateRpcResponse($response);
            return;
        }

        // 3. Fallback to the old v1 version
        $response = $client->post($url . '/v1/send/' . $identifier, [
            'json' => ['message' => $message]
        ]);
        
        if ($response->getStatusCode() !== 200 && $response->getStatusCode() !== 201) {
            throw new MessageTransmissionException("Signal gateway v1 failed with status " . $response->getStatusCode());
        }
    }

    private function validateV2Response($response): void {
        $status = $response->getStatusCode();
        $body = $response->getBody();
        $json = json_decode($body, true);
        if ($status !== 201 || !isset($json['timestamp'])) {
            throw new MessageTransmissionException("Error in Signal v2: status=$status, body=$body");
        }
    }

    private function validateRpcResponse($response): void {
        $body = $response->getBody();
        $json = json_decode($body, true);
        if (($json['jsonrpc'] ?? null) !== '2.0' || isset($json['error'])) {
            throw new MessageTransmissionException("Error in Signal RPC: " . $body);
        }
    }

    #[\Override]
    public function cliConfigure(InputInterface $input, OutputInterface $output): int {
        $helper = new QuestionHelper();
        $settings = $this->createSettings();

        $url = $helper->ask($input, $output, new Question($settings->fields[0]->prompt, $settings->fields[0]->default));
        $this->setUrl($url);

        $account = $helper->ask($input, $output, new Question($settings->fields[1]->prompt, ''));
        if (empty($account)) {
            $account = self::ACCOUNT_UNNECESSARY;
            $output->writeln('Account not set, using gateway defaults.');
        }
        $this->setAccount($account);

        return 0;
    }
}
<!-- gh-comment-id:3813715164 --> @oleua commented on GitHub (Jan 28, 2026): In the v32 I do not use anymore the `SIGNAL_CLI_DBUS_REST_API_ACCOUNT` variable in `config.php`. I have adapted the configuration to make it compatible with `bbernhard/signal-cli-rest-api`, which I use: In `/apps/twofactor_gateway/lib/Provider/Channel/Signal/Gateway.php`: ``` <?php // modified by me declare(strict_types=1); /** * SPDX-FileCopyrightText: 2024 Christoph Wurst <christoph@winzerhof-wurst.at> * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\TwoFactorGateway\Provider\Channel\Signal; use OCA\TwoFactorGateway\Exception\MessageTransmissionException; use OCA\TwoFactorGateway\Provider\FieldDefinition; use OCA\TwoFactorGateway\Provider\Gateway\AGateway; use OCA\TwoFactorGateway\Provider\Settings; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Http\Client\IClientService; use OCP\IAppConfig; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; /** * An integration of https://github.com/bbernhard/signal-cli-rest-api * with backward compatibility. * @method string getUrl() * @method AGateway setUrl(string $url) * @method string getAccount() * @method AGateway setAccount(string $account) */ class Gateway extends AGateway { public const ACCOUNT_UNNECESSARY = 'unnecessary'; public function __construct( public IAppConfig $appConfig, private IClientService $clientService, private ITimeFactory $timeFactory, private LoggerInterface $logger, ) { parent::__construct($appConfig); } #[\Override] public function createSettings(): Settings { return new Settings( name: 'Signal', instructions: 'The gateway can send authentication codes to your Signal app.', fields: [ new FieldDefinition( field: 'url', prompt: 'Please enter the URL of the Signal gateway:', default: 'http://localhost:9922', ), new FieldDefinition( field: 'account', prompt: 'Please enter the registered Signal number (e.g. +380...):', ), ] ); } #[\Override] public function send(string $identifier, string $message, array $extra = []): void { $client = $this->clientService->newClient(); $url = rtrim($this->getUrl(), '/'); $account = $this->getAccount(); // 1. Identifying the gateway vial /v1/about try { $aboutResponse = $client->get($url . '/v1/about'); if ($aboutResponse->getStatusCode() === 200) { $body = $aboutResponse->getBody(); $jsonAbout = json_decode($body, true); $versions = $jsonAbout['versions'] ?? []; // Якщо це bbernhard/signal-cli-rest-api (підтримує v2) if (is_array($versions) && in_array('v2', $versions)) { $payload = [ 'text_mode' => 'styled', 'message' => $message, 'recipients' => [$identifier], ]; if ($account !== self::ACCOUNT_UNNECESSARY && !empty($account)) { $payload['number'] = $account; } $response = $client->post($url . '/v2/send', ['json' => $payload]); $this->validateV2Response($response); return; } } } catch (\Exception $e) { $this->logger->error("Signal Gateway check failed: " . $e->getMessage()); } // 2. If v2 hasn't worked use JSON RPC (new standard since NC32) $rpcResponse = $client->post($url . '/api/v1/rpc', [ 'http_errors' => false, 'json' => [ 'jsonrpc' => '2.0', 'method' => 'version', 'id' => 'v_' . $this->timeFactory->getTime(), ], ]); if (in_array($rpcResponse->getStatusCode(), [200, 201])) { $params = [ 'message' => $message, 'recipient' => $identifier, 'account' => $account, ]; $response = $client->post($url . '/api/v1/rpc', [ 'json' => [ 'jsonrpc' => '2.0', 'method' => 'send', 'id' => 'msg_' . $this->timeFactory->getTime(), 'params' => $params, ], ]); $this->validateRpcResponse($response); return; } // 3. Fallback to the old v1 version $response = $client->post($url . '/v1/send/' . $identifier, [ 'json' => ['message' => $message] ]); if ($response->getStatusCode() !== 200 && $response->getStatusCode() !== 201) { throw new MessageTransmissionException("Signal gateway v1 failed with status " . $response->getStatusCode()); } } private function validateV2Response($response): void { $status = $response->getStatusCode(); $body = $response->getBody(); $json = json_decode($body, true); if ($status !== 201 || !isset($json['timestamp'])) { throw new MessageTransmissionException("Error in Signal v2: status=$status, body=$body"); } } private function validateRpcResponse($response): void { $body = $response->getBody(); $json = json_decode($body, true); if (($json['jsonrpc'] ?? null) !== '2.0' || isset($json['error'])) { throw new MessageTransmissionException("Error in Signal RPC: " . $body); } } #[\Override] public function cliConfigure(InputInterface $input, OutputInterface $output): int { $helper = new QuestionHelper(); $settings = $this->createSettings(); $url = $helper->ask($input, $output, new Question($settings->fields[0]->prompt, $settings->fields[0]->default)); $this->setUrl($url); $account = $helper->ask($input, $output, new Question($settings->fields[1]->prompt, '')); if (empty($account)) { $account = self::ACCOUNT_UNNECESSARY; $output->writeln('Account not set, using gateway defaults.'); } $this->setAccount($account); return 0; } } ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/twofactor_gateway-nextcloud#121
No description provided.