OTPClient::verifyCode()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 27
nc 2
nop 2
dl 0
loc 44
rs 9.488
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Utilities for sending OTP text messages
5
 *
6
 * @package tvdijen/simplesamlphp-module-cmdotcom
7
 */
8
9
declare(strict_types=1);
10
11
namespace SimpleSAML\Module\cmdotcom\Utils;
12
13
use GuzzleHttp\Client as GuzzleClient;
14
use Psr\Http\Message\ResponseInterface;
15
use SimpleSAML\{Configuration, Session};
16
use SimpleSAML\Assert\Assert;
17
use SimpleSAML\Locale\Translate;
18
use SimpleSAML\XHTML\Template;
19
20
class OTPClient
21
{
22
    /** @var string */
23
    public const API_BASE = 'https://api.cmtelecom.com';
24
25
    /** @var string */
26
    public const HEADER = 'X-CM-ProductToken';
27
28
    /** @var \SimpleSAML\Configuration */
29
    protected Configuration $config;
30
31
32
    /**
33
     * @param \SimpleSAML\Configuration $config The configuration to use.
34
     */
35
    public function __construct(Configuration $config)
36
    {
37
        $this->config = $config;
38
    }
39
40
41
    /**
42
     * Send OTP code
43
     *
44
     * @param array $state
45
     * @return \Psr\Http\Message\ResponseInterface
46
     */
47
    public function sendCode(array $state): ResponseInterface
48
    {
49
        Assert::keyExists($state, 'cmdotcom:productToken', 'Missing required REST API key for the cm.com service.');
50
        Assert::keyExists($state, 'cmdotcom:recipient');
51
        Assert::keyExists($state, 'cmdotcom:originator');
52
        Assert::keyExists($state, 'cmdotcom:codeLength');
53
        Assert::keyExists($state, 'cmdotcom:validFor');
54
        Assert::keyExists($state, 'cmdotcom:allowPush');
55
56
        // Validate product token
57
        $productToken = $state['cmdotcom:productToken'];
58
        Assert::notNull(
59
            $productToken,
60
            'Missing required REST API key for the cm.com service.',
61
        );
62
        Assert::uuid($productToken);
63
64
        // Validate appKey
65
        $allowPush = $state['cmdotcom:allowPush'];
66
        $appKey = null;
67
        if ($allowPush === true) {
68
            $appKey = $state['cmdotcom:appKey'];
69
            Assert::notNull(
70
                $appKey,
71
                'Missing required appKey for use with push notification.',
72
            );
73
            Assert::uuid($appKey);
74
        }
75
76
        // Validate originator
77
        $originator = $state['cmdotcom:originator'];
78
        if (preg_match('/[0-9]+/', $originator)) {
79
            Assert::maxLength(
80
                $originator,
81
                16,
82
                'A numeric originator must represent a phonenumber and can contain a maximum of 16 digits.',
83
            );
84
        } else {
85
            // TODO: figure out what characters are allowed and write a regex.
86
            // So far 'A-Z', 'a-z', '0-9', ' ' and '-' are known to be accepted
87
            //Assert::alnum(str_replace(' ', '', $originator));
88
            Assert::lengthBetween(
89
                $originator,
90
                3,
91
                11,
92
                'An alphanumeric originator can contain a minimum of 2 and a maximum of 11 characters.',
93
            );
94
        }
95
96
        // Validate OTP length
97
        $codeLength = $state['cmdotcom:codeLength'];
98
        Assert::range($codeLength, 4, 10);
99
100
        // Validate recipient
101
        $recipient = $state['cmdotcom:recipient'];
102
        Assert::numeric($recipient);
103
        Assert::maxLength(
104
            $recipient,
105
            16,
106
            'A recipient must represent a phonenumber and can contain a maximum of 16 digits.',
107
        );
108
109
        // Validate validFor
110
        $validFor = $state['cmdotcom:validFor'];
111
        Assert::range($validFor, 10, 3600);
112
113
        // Translate text-message
114
        // Initializating a Localization is a dirty hack to make translateSingularGettext work.
115
        new Template($this->config, 'cmdotcom:message.twig');
116
        $message = Translate::translateSingularGettext(
117
            '{code}
118
Enter this verification code when asked during the authentication process.',
119
        );
120
121
        $options = [
122
            'base_uri' => self::API_BASE,
123
            //'debug' => true,
124
            'headers' => [
125
                'Content-Type' => 'application/json',
126
                self::HEADER => $productToken,
127
            ],
128
            'http_errors' => false,
129
            'timeout' => 3.0,
130
        ];
131
132
        $client = new GuzzleClient($options);
133
        $json = [
134
            'recipient' => $recipient,
135
            'sender' => $originator,
136
            'length' => $codeLength,
137
            'expiry' => $validFor,
138
            'message' => $message,
139
        ];
140
141
        if ($allowPush === true) {
142
            $json += ['allowPush' => $allowPush, 'appKey' => $appKey];
143
        }
144
145
        return $client->request(
146
            'POST',
147
            '/v1.0/otp/generate',
148
            [
149
                'json' => $json,
150
            ],
151
        );
152
    }
153
154
155
    /**
156
     * Verify OTP code
157
     *
158
     * @param array $state
159
     * @param string $code
160
     * @return \Psr\Http\Message\ResponseInterface
161
     */
162
    public function verifyCode(array $state, string $code): ResponseInterface
163
    {
164
        Assert::keyExists($state, 'cmdotcom:reference');
165
        Assert::keyExists($state, 'cmdotcom:productToken');
166
167
        // Validate reference
168
        $reference = $state['cmdotcom:reference'];
169
        Assert::uuid($reference);
170
171
        // Validate product token
172
        $productToken = $state['cmdotcom:productToken'];
173
        Assert::notNull(
174
            $productToken,
175
            'Missing required REST API key for the cm.com service.',
176
        );
177
        Assert::uuid($productToken);
178
179
        $options = [
180
            'base_uri' => self::API_BASE,
181
            //'debug' => true,
182
            'headers' => [
183
                'Content-Type' => 'application/json',
184
                self::HEADER => $productToken,
185
            ],
186
            'http_errors' => false,
187
            'timeout' => 3.0,
188
        ];
189
190
        $proxy = $this->config->getOptionalString('proxy', null);
191
        if ($proxy !== null) {
192
            $options += ['proxy' => ['http' => $proxy, 'https' => $proxy]];
193
        }
194
195
        $client = new GuzzleClient($options);
196
        $json = [
197
            'id' => $reference,
198
            'code' => $code,
199
        ];
200
201
        return $client->request(
202
            'POST',
203
            '/v1.0/otp/verify',
204
            [
205
                'json' => $json,
206
            ],
207
        );
208
    }
209
}
210