OTP   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 166
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 62
dl 0
loc 166
rs 10
c 0
b 0
f 0
wmc 17

3 Methods

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