Passed
Push — master ( b215e3...34e661 )
by Tim
11:08
created

OTP::__construct()   C

Complexity

Conditions 9
Paths 256

Size

Total Lines 47
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 20
c 2
b 0
f 0
dl 0
loc 47
rs 6.5222
cc 9
nc 256
nop 2
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
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 message from the configuration
85
        if (isset($config['message'])) {
86
            $this->message = $config['message'];
87
        }
88
89
        // Retrieve the optional code length from the configuration
90
        if (isset($config['codeLength'])) {
91
            $this->codeLength = $config['codeLength'];
92
        }
93
94
        // Retrieve the optional attribute name that holds the mobile phone number
95
        if (isset($config['mobilePhoneAttribute'])) {
96
            $this->mobilePhoneAttribute = $config['mobilePhoneAttribute'];
97
        }
98
99
        // Retrieve the optional validFor
100
        if (isset($config['validFor'])) {
101
            $this->validFor = $config->validFor;
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);
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:message'] = $this->message;
142
        $state['cmdotcom:allowPush'] = $this->allowPush;
143
144
        if ($this->allowPush === true) {
145
            $state['cmdotcom:appKey'] = $this->appKey;
146
        }
147
148
        // Save state and redirect
149
        $id = Auth\State::saveState($state, 'cmdotcom:request');
150
        $url = Module::getModuleURL('cmdotcom/sendCode');
151
152
        $httpUtils = new Utils\HTTP();
153
        $httpUtils->redirectTrustedURL($url, ['AuthState' => $id]);
154
    }
155
156
157
    /**
158
     * Retrieve the mobile phone attribute from the state
159
     *
160
     * @param array $state
161
     * @return string
162
     * @throws \RuntimeException if no attribute with a mobile phone number is present.
163
     */
164
    protected function getMobilePhoneAttribute(array $state): string
165
    {
166
        Assert::keyExists($state, 'Attributes');
167
        Assert::keyExists(
168
            $state['Attributes'],
169
            $this->mobilePhoneAttribute,
170
            sprintf(
171
                "cmdotcom:OTP: Missing attribute '%s', which is needed to send an SMS.",
172
                $this->mobilePhoneAttribute,
173
            ),
174
        );
175
176
        return $state['Attributes'][$this->mobilePhoneAttribute][0];
177
    }
178
}
179