Passed
Push — master ( de0c56...296abe )
by Tim
03:49
created

OTP::getMobilePhoneAttribute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 13
rs 10
cc 1
nc 1
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\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 attribute containing the user's mobile phone number
32
    private string $mobilePhoneAttribute = 'mobile';
33
34
    // The number of seconds an SMS-code can be used for authentication
35
    private int $validFor = 180;
36
37
    // The number digits to use for the OTP between 4 an 10
38
    private int $codeLength = 5;
39
40
    // Whether or not the OTP-code should be pushed to an app on the device
41
    private bool $allowPush = false;
42
43
    // The app key to be used when allowPush is set to true
44
    private ?string $appKey = null;
45
46
    // The default region (CLDR-format) to use when parsing recipient phone numbers
47
    private string $defaultRegion = 'ZZ';
48
49
50
    /**
51
     * Initialize SMS OTP filter.
52
     *
53
     * Validates and parses the configuration.
54
     *
55
     * @param array $config Configuration information.
56
     * @param mixed $reserved For future use.
57
     *
58
     * @throws \Exception if the required REST API key is missing.
59
     */
60
    public function __construct(array $config, $reserved)
61
    {
62
        parent::__construct($config, $reserved);
63
64
        // Retrieve the mandatory product token from the configuration
65
        if (isset($config['productToken'])) {
66
            $this->productToken = $config['productToken'];
67
        }
68
69
        // Retrieve the optional allowPush from the configuration
70
        if (isset($config['allowPush'])) {
71
            $this->allowPush = $config['allowPush'];
72
        }
73
74
        // Retrieve the optional app key from the configuration
75
        if (isset($config['appKey'])) {
76
            $this->appKey = $config['appKey'];
77
        }
78
79
        // Retrieve the optional originator from the configuration
80
        if (isset($config['originator'])) {
81
            $this->originator = $config['originator'];
82
        }
83
84
        // Retrieve the optional code length from the configuration
85
        if (isset($config['codeLength'])) {
86
            $this->codeLength = $config['codeLength'];
87
        }
88
89
        // Retrieve the optional attribute name that holds the mobile phone number
90
        if (isset($config['mobilePhoneAttribute'])) {
91
            $this->mobilePhoneAttribute = $config['mobilePhoneAttribute'];
92
        }
93
94
        // Retrieve the optional validFor
95
        if (isset($config['validFor'])) {
96
            $this->validFor = $config['validFor'];
97
        }
98
99
        // Retrieve the optional defaultRegion
100
        if (isset($config['defaultRegion'])) {
101
            $this->defaultRegion = $config['defaultRegion'];
102
        }
103
104
        Assert::notEmpty(
105
            $this->mobilePhoneAttribute,
106
            'mobilePhoneAttribute cannot be an empty string.',
107
        );
108
    }
109
110
111
    /**
112
     * Process a authentication response
113
     *
114
     * This function saves the state, and redirects the user to the page where the user can enter the OTP
115
     * code sent to them.
116
     *
117
     * @param array &$state The state of the response.
118
     */
119
    public function process(array &$state): void
120
    {
121
        // user interaction necessary. Throw exception on isPassive request
122
        if (isset($state['isPassive']) && $state['isPassive'] === true) {
123
            throw new Error\NoPassive(
124
                Constants::STATUS_REQUESTER,
125
                'Unable to enter verification code on passive request.'
126
            );
127
        }
128
129
        // Retrieve the user's mobile phone number
130
        $recipient = $this->getMobilePhoneAttribute($state);
131
132
        // Sanitize the user's mobile phone number
133
        $phoneNumberUtils = new PhoneNumberUtils();
134
        $recipient = $phoneNumberUtils->sanitizePhoneNumber($recipient, $this->defaultRegion);
135
136
        $state['cmdotcom:productToken'] = $this->productToken;
137
        $state['cmdotcom:originator'] = $this->originator;
138
        $state['cmdotcom:recipient'] = $recipient;
139
        $state['cmdotcom:validFor'] = $this->validFor;
140
        $state['cmdotcom:codeLength'] = $this->codeLength;
141
        $state['cmdotcom:allowPush'] = $this->allowPush;
142
143
        if ($this->allowPush === true) {
144
            $state['cmdotcom:appKey'] = $this->appKey;
145
        }
146
147
        // Save state and redirect
148
        $id = Auth\State::saveState($state, 'cmdotcom:request');
149
        $url = Module::getModuleURL('cmdotcom/sendCode');
150
151
        $httpUtils = new Utils\HTTP();
152
        $httpUtils->redirectTrustedURL($url, ['AuthState' => $id]);
153
    }
154
155
156
    /**
157
     * Retrieve the mobile phone attribute from the state
158
     *
159
     * @param array $state
160
     * @return string
161
     * @throws \RuntimeException if no attribute with a mobile phone number is present.
162
     */
163
    protected function getMobilePhoneAttribute(array $state): string
164
    {
165
        Assert::keyExists($state, 'Attributes');
166
        Assert::keyExists(
167
            $state['Attributes'],
168
            $this->mobilePhoneAttribute,
169
            sprintf(
170
                "cmdotcom:OTP: Missing attribute '%s', which is needed to send an SMS.",
171
                $this->mobilePhoneAttribute,
172
            ),
173
        );
174
175
        return $state['Attributes'][$this->mobilePhoneAttribute][0];
176
    }
177
}
178