Passed
Push — master ( 9af2e4...b0d4ae )
by Tim
02:50
created

OTP   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 162
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 15
eloc 60
c 4
b 0
f 0
dl 0
loc 162
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
D __construct() 0 52 10
A process() 0 35 4
A getMobilePhoneAttribute() 0 13 1
1
<?php
2
3
/**
4
 * SMS Authentication Processing filter
5
 *
6
 * Filter for requesting the user's SMS-based OTP.
7
 *
8
 * @package tvdijen/simplesamlphp-module-cmdotcom
9
 */
10
11
declare(strict_types=1);
12
13
namespace SimpleSAML\Module\cmdotcom\Auth\Process;
14
15
use RuntimeException;
16
use SAML2\Constants;
17
use SimpleSAML\{Auth, Configuration, Logger, Module, Utils};
18
use SimpleSAML\Assert\Assert;
19
use SimpleSAML\Module\cmdotcom\Utils\PhoneNumber as PhoneNumberUtils;
20
use SimpleSAML\Module\saml\Error;
21
use UnexpectedValueException;
22
23
class OTP extends Auth\ProcessingFilter
24
{
25
    // The REST API key for the cm.com SMS service (also called Product Token)
26
    private ?string $productToken = null;
27
28
    // The originator for the SMS
29
    private string $originator = 'Example';
30
31
    // The content of the SMS
32
    private string $message = '{code}';
33
34
    // The attribute containing the user's mobile phone number
35
    private string $mobilePhoneAttribute = 'mobile';
36
37
    // The number of seconds an SMS-code can be used for authentication
38
    private int $validFor = 180;
39
40
    // The number digits to use for the OTP between 4 an 10
41
    private int $codeLength = 5;
42
43
    // Whether or not the OTP-code should be pushed to an app on the device
44
    private bool $allowPush = false;
45
46
    // The app key to be used when allowPush is set to true
47
    private ?string $appKey = null;
48
49
    // The default region (CLDR-format) to use when parsing recipient phone numbers
50
    private string $defaultRegion = 'ZZ';
51
52
53
    /**
54
     * Initialize SMS OTP filter.
55
     *
56
     * Validates and parses the configuration.
57
     *
58
     * @param array $config Configuration information.
59
     * @param mixed $reserved For future use.
60
     *
61
     * @throws \Exception if the required REST API key is missing.
62
     */
63
    public function __construct(array $config, $reserved)
64
    {
65
        parent::__construct($config, $reserved);
66
67
        // Retrieve the mandatory product token from the configuration
68
        if (isset($config['productToken'])) {
69
            $this->productToken = $config['productToken'];
70
        }
71
72
        // Retrieve the optional allowPush from the configuration
73
        if (isset($config['allowPush'])) {
74
            $this->allowPush = $config['allowPush'];
75
        }
76
77
        // Retrieve the optional app key from the configuration
78
        if (isset($config['appKey'])) {
79
            $this->appKey = $config['appKey'];
80
        }
81
82
        // Retrieve the optional originator from the configuration
83
        if (isset($config['originator'])) {
84
            $this->originator = $config['originator'];
85
        }
86
87
        // Retrieve the optional message from the configuration
88
        if (isset($config['message'])) {
89
            $this->message = $config['message'];
90
        }
91
92
        // Retrieve the optional code length from the configuration
93
        if (isset($config['codeLength'])) {
94
            $this->codeLength = $config['codeLength'];
95
        }
96
97
        // Retrieve the optional attribute name that holds the mobile phone number
98
        if (isset($config['mobilePhoneAttribute'])) {
99
            $this->mobilePhoneAttribute = $config['mobilePhoneAttribute'];
100
        }
101
102
        // Retrieve the optional validFor
103
        if (isset($config['validFor'])) {
104
            $this->validFor = $config['validFor'];
105
        }
106
107
        // Retrieve the optional defaultRegion
108
        if (isset($config['defaultRegion'])) {
109
            $this->defaultRegion = $config['defaultRegion'];
110
        }
111
112
        Assert::notEmpty(
113
            $this->mobilePhoneAttribute,
114
            'mobilePhoneAttribute cannot be an empty string.',
115
        );
116
    }
117
118
119
    /**
120
     * Process a authentication response
121
     *
122
     * This function saves the state, and redirects the user to the page where the user can enter the OTP
123
     * code sent to them.
124
     *
125
     * @param array &$state The state of the response.
126
     */
127
    public function process(array &$state): void
128
    {
129
        // user interaction necessary. Throw exception on isPassive request
130
        if (isset($state['isPassive']) && $state['isPassive'] === true) {
131
            throw new Error\NoPassive(
132
                Constants::STATUS_REQUESTER,
133
                'Unable to enter verification code on passive request.'
134
            );
135
        }
136
137
        // Retrieve the user's mobile phone number
138
        $recipient = $this->getMobilePhoneAttribute($state);
139
140
        // Sanitize the user's mobile phone number
141
        $phoneNumberUtils = new PhoneNumberUtils();
142
        $recipient = $phoneNumberUtils->sanitizePhoneNumber($recipient, $this->defaultRegion);
143
144
        $state['cmdotcom:productToken'] = $this->productToken;
145
        $state['cmdotcom:originator'] = $this->originator;
146
        $state['cmdotcom:recipient'] = $recipient;
147
        $state['cmdotcom:validFor'] = $this->validFor;
148
        $state['cmdotcom:codeLength'] = $this->codeLength;
149
        $state['cmdotcom:message'] = $this->message;
150
        $state['cmdotcom:allowPush'] = $this->allowPush;
151
152
        if ($this->allowPush === true) {
153
            $state['cmdotcom:appKey'] = $this->appKey;
154
        }
155
156
        // Save state and redirect
157
        $id = Auth\State::saveState($state, 'cmdotcom:request');
158
        $url = Module::getModuleURL('cmdotcom/sendCode');
159
160
        $httpUtils = new Utils\HTTP();
161
        $httpUtils->redirectTrustedURL($url, ['AuthState' => $id]);
162
    }
163
164
165
    /**
166
     * Retrieve the mobile phone attribute from the state
167
     *
168
     * @param array $state
169
     * @return string
170
     * @throws \RuntimeException if no attribute with a mobile phone number is present.
171
     */
172
    protected function getMobilePhoneAttribute(array $state): string
173
    {
174
        Assert::keyExists($state, 'Attributes');
175
        Assert::keyExists(
176
            $state['Attributes'],
177
            $this->mobilePhoneAttribute,
178
            sprintf(
179
                "cmdotcom:OTP: Missing attribute '%s', which is needed to send an SMS.",
180
                $this->mobilePhoneAttribute,
181
            ),
182
        );
183
184
        return $state['Attributes'][$this->mobilePhoneAttribute][0];
185
    }
186
}
187