WebAuthn::__construct()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 15
c 2
b 0
f 0
nc 2
nop 2
dl 0
loc 21
rs 9.7666
1
<?php
2
3
/**
4
 * FIDO2/WebAuthn Authentication Processing filter
5
 *
6
 * Filter for registering or authenticating with a FIDO2/WebAuthn token after
7
 * having authenticated with the primary authsource.
8
 *
9
 * @author Stefan Winter <[email protected]>
10
 * @package SimpleSAMLphp
11
 */
12
13
declare(strict_types=1);
14
15
namespace SimpleSAML\Module\webauthn\Auth\Process;
16
17
use SimpleSAML\Auth;
18
use SimpleSAML\Configuration;
19
use SimpleSAML\Logger;
20
use SimpleSAML\Module;
21
use SimpleSAML\Module\webauthn\WebAuthn\StateData;
22
use SimpleSAML\Module\webauthn\WebAuthn\StaticProcessHelper;
23
use SimpleSAML\Session;
24
25
class WebAuthn extends Auth\ProcessingFilter
26
{
27
    /**
28
     * @var boolean should new users be considered as enabled by default?
29
     */
30
    private bool $defaultEnabled;
31
32
    /**
33
     * @var boolean switch that determines how 'toggle' will be used, if true then value of 'toggle'
34
     *              will mean whether to trigger (true) or not (false) the webauthn authentication,
35
     *              if false then $toggle means whether to switch the value of $defaultEnabled and then use that
36
     */
37
    private bool $force;
38
39
    /**
40
     * @var string an attribute which is associated with 'force' because it determines its meaning,
41
     *              it either simply means whether to trigger webauthn authentication or switch the default settings,
42
     *              if null (was not sent as attribute) then the information from database is used
43
     */
44
    private string $toggleAttrib;
45
46
    /**
47
     * @var bool a bool that determines whether to use local database or not
48
     */
49
    private bool $useDatabase;
50
51
    /**
52
     * @var string|null AuthnContextClassRef
53
     */
54
    private ?string $authnContextClassRef = null;
55
56
    /**
57
     * An object with all the parameters that will be needed in the process
58
     *
59
     * @var Module\webauthn\WebAuthn\StateData
60
     */
61
    private StateData $stateData;
62
63
    /**
64
     * Maximum age of second-factor authentication in authproc
65
     */
66
    private int $SecondFactorMaxAge;
67
68
    /**
69
     * Initialize filter.
70
     *
71
     * Validates and parses the configuration.
72
     *
73
     * @param array $config Configuration information.
74
     * @param mixed $reserved For future use.
75
     *
76
     * @throws \SimpleSAML\Error\Exception if the configuration is not valid.
77
     */
78
    public function __construct(array $config, $reserved)
79
    {
80
        parent::__construct($config, $reserved);
81
82
        $moduleConfig = Configuration::getOptionalConfig('module_webauthn.php')->toArray();
83
84
        $initialStateData = new Module\webauthn\WebAuthn\StateData();
85
        Module\webauthn\Controller\WebAuthn::loadModuleConfig($moduleConfig, $initialStateData);
86
        $this->stateData = $initialStateData;
87
88
        $this->force = $config['force'] ?? true;
89
        $this->toggleAttrib = $config['attrib_toggle'] ?? 'toggle';
90
        $this->useDatabase = $config['use_database'] ?? true;
91
        $this->defaultEnabled = $config['default_enable'] ?? false;
92
        $this->authnContextClassRef = $config['authncontextclassref'] ?? null;
93
        $this->SecondFactorMaxAge = $config['secondfactormaxage'] ?? -1;
94
95
        if (array_key_exists('use_inflow_registration', $moduleConfig['registration'])) {
96
            $this->stateData->useInflowRegistration = $moduleConfig['registration']['use_inflow_registration'];
97
        } else {
98
            $this->stateData->useInflowRegistration = true;
99
        }
100
    }
101
102
    /**
103
     * Process a authentication response
104
     *
105
     * This function saves the state, and redirects the user to the page where
106
     * the user can register or authenticate with his token.
107
     *
108
     * @param array &$state The state of the response.
109
     *
110
     * @return void
111
     */
112
    public function process(array &$state): void
113
    {
114
        if (!array_key_exists($this->stateData->usernameAttrib, $state['Attributes'])) {
115
            Logger::warning('webauthn: cannot determine if user needs second factor, missing attribute "' .
116
                    $this->stateData->usernameAttrib . '".');
117
            return;
118
        }
119
120
        $state['saml:AuthnContextClassRef'] = $this->authnContextClassRef ??
121
                'urn:rsa:names:tc:SAML:2.0:ac:classes:FIDO';
122
        Logger::debug('webauthn: userid: ' . $state['Attributes'][$this->stateData->usernameAttrib][0]);
123
124
        $localToggle = !empty($state['Attributes'][$this->toggleAttrib]) &&
125
            !empty($state['Attributes'][$this->toggleAttrib][0]);
126
127
        if (
128
                $this->stateData->store->is2FAEnabled(
129
                    $state['Attributes'][$this->stateData->usernameAttrib][0],
130
                    $this->defaultEnabled,
131
                    $this->useDatabase,
132
                    $localToggle,
133
                    $this->force,
134
                ) === false
135
        ) {
136
            // nothing to be done here, end authprocfilter processing
137
            return;
138
        }
139
140
        if // did we do Passwordless mode successfully before?
141
        (
142
                isset($state['Attributes']['internal:FIDO2PasswordlessAuthentication']) &&
143
                // phpcs:ignore Generic.Files.LineLength.TooLong
144
                $state['Attributes']['internal:FIDO2PasswordlessAuthentication'][0] == $state['Attributes'][$this->stateData->usernameAttrib][0]
145
        ) {
146
            // then no need to trigger a second 2-Factor via authproc
147
            // just delete the internal attribute then
148
            unset($state['Attributes']['internal:FIDO2PasswordlessAuthentication']);
149
            return;
150
        }
151
        $session = Session::getSessionFromRequest();
152
        $lastSecondFactor = $session->getData("DateTime", 'LastSuccessfulSecondFactor');
153
        if // do we need to do secondFactor in interval, or even every time?
154
           // we skip only if an interval is configured AND we did successfully authenticate,
155
           // AND are within the interval
156
        (
157
                $this->SecondFactorMaxAge >= 0 && $lastSecondFactor instanceof \DateTime
158
        ) {
159
            $interval = $lastSecondFactor->diff(new \DateTime());
160
            if ($interval->invert == 1) {
161
                throw new \Exception("We are talking to a future self. Amazing.");
162
            }
163
            // phpcs:ignore Generic.Files.LineLength.TooLong
164
            $totalAge = $interval->s + 60 * $interval->i + 3600 * $interval->h + 86400 * $interval->d + 86400 * 30 * $interval->m + 86400 * 365 * $interval->y;
165
            if ($totalAge < $this->SecondFactorMaxAge) { // we are within the interval indeed, skip calling the AuthProc
166
                return;
167
            }
168
        }
169
        StaticProcessHelper::prepareState($this->stateData, $state);
170
        StaticProcessHelper::saveStateAndRedirect($state);
171
    }
172
}
173