Passed
Push — master ( d3fff8...1cbd78 )
by Tim
03:42
created

OTP::process()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 44
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 23
c 1
b 0
f 0
dl 0
loc 44
rs 8.4444
cc 8
nc 7
nop 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\HTTP\RunnableResponse;
20
use SimpleSAML\Module\cmdotcom\Utils\PhoneNumber as PhoneNumberUtils;
21
use SimpleSAML\Module\saml\Error;
22
use UnexpectedValueException;
23
24
class OTP extends Auth\ProcessingFilter
25
{
26
    // The REST API key for the cm.com SMS service (also called Product Token)
27
    private ?string $productToken = null;
28
29
    // The originator for the SMS
30
    private string $originator = 'Example';
31
32
    // The attribute containing the user's mobile phone number
33
    private string $mobilePhoneAttribute = 'mobile';
34
35
    // The number of seconds an SMS-code can be used for authentication
36
    private int $validFor = 180;
37
38
    // The number digits to use for the OTP between 4 an 10
39
    private int $codeLength = 5;
40
41
    // Whether or not the OTP-code should be pushed to an app on the device
42
    private bool $allowPush = false;
43
44
    // The app key to be used when allowPush is set to true
45
    private ?string $appKey = null;
46
47
    // The default region (CLDR-format) to use when parsing recipient phone numbers
48
    private string $defaultRegion = 'ZZ';
49
50
51
    /**
52
     * Initialize SMS OTP filter.
53
     *
54
     * Validates and parses the configuration.
55
     *
56
     * @param array $config Configuration information.
57
     * @param mixed $reserved For future use.
58
     *
59
     * @throws \Exception if the required REST API key is missing.
60
     */
61
    public function __construct(array $config, $reserved)
62
    {
63
        parent::__construct($config, $reserved);
64
65
        // Retrieve the mandatory product token from the configuration
66
        if (isset($config['productToken'])) {
67
            $this->productToken = $config['productToken'];
68
        }
69
70
        // Retrieve the optional allowPush from the configuration
71
        if (isset($config['allowPush'])) {
72
            $this->allowPush = $config['allowPush'];
73
        }
74
75
        // Retrieve the optional app key from the configuration
76
        if (isset($config['appKey'])) {
77
            $this->appKey = $config['appKey'];
78
        }
79
80
        // Retrieve the optional originator from the configuration
81
        if (isset($config['originator'])) {
82
            $this->originator = $config['originator'];
83
        }
84
85
        // Retrieve the optional code length from the configuration
86
        if (isset($config['codeLength'])) {
87
            $this->codeLength = $config['codeLength'];
88
        }
89
90
        // Retrieve the optional attribute name that holds the mobile phone number
91
        if (isset($config['mobilePhoneAttribute'])) {
92
            $this->mobilePhoneAttribute = $config['mobilePhoneAttribute'];
93
        }
94
95
        // Retrieve the optional validFor
96
        if (isset($config['validFor'])) {
97
            $this->validFor = $config['validFor'];
98
        }
99
100
        // Retrieve the optional defaultRegion
101
        if (isset($config['defaultRegion'])) {
102
            $this->defaultRegion = $config['defaultRegion'];
103
        }
104
105
        Assert::notEmpty(
106
            $this->mobilePhoneAttribute,
107
            'mobilePhoneAttribute cannot be an empty string.',
108
        );
109
    }
110
111
112
    /**
113
     * Process a authentication response
114
     *
115
     * This function saves the state, and redirects the user to the page where the user can enter the OTP
116
     * code sent to them.
117
     *
118
     * @param array &$state The state of the response.
119
     */
120
    public function process(array &$state): void
121
    {
122
        // user interaction necessary. Throw exception on isPassive request
123
        if (isset($state['isPassive']) && $state['isPassive'] === true) {
124
            throw new Error\NoPassive(
125
                Constants::STATUS_REQUESTER,
126
                'Unable to enter verification code on passive request.'
127
            );
128
        }
129
130
        // See if we're reauthenticating
131
        if (isset($state['cmdotcom:authenticated']) && $state['cmdotcom: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