WebAuthn   A
last analyzed

Complexity

Total Complexity 12

Size/Duplication

Total Lines 146
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 58
c 5
b 0
f 0
dl 0
loc 146
rs 10
wmc 12

2 Methods

Rating   Name   Duplication   Size   Complexity  
B process() 0 59 10
A __construct() 0 21 2
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 \SimpleSAML\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
    /**
70
     * Initialize filter.
71
     *
72
     * Validates and parses the configuration.
73
     *
74
     * @param array $config Configuration information.
75
     * @param mixed $reserved For future use.
76
     *
77
     * @throws \SimpleSAML\Error\Exception if the configuration is not valid.
78
     */
79
    public function __construct(array $config, $reserved)
80
    {
81
        parent::__construct($config, $reserved);
82
83
        $moduleConfig = Configuration::getOptionalConfig('module_webauthn.php')->toArray();
84
85
        $initialStateData = new Module\webauthn\WebAuthn\StateData();
86
        Module\webauthn\Controller\WebAuthn::loadModuleConfig($moduleConfig, $initialStateData);
0 ignored issues
show
Bug introduced by
The type SimpleSAML\Module\webauthn\Controller\WebAuthn was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
87
        $this->stateData = $initialStateData;
88
89
        $this->force = $config['force'] ?? true;
90
        $this->toggleAttrib = $config['attrib_toggle'] ?? 'toggle';
91
        $this->useDatabase = $config['use_database'] ?? true;
92
        $this->defaultEnabled = $config['default_enable'] ?? false;
93
        $this->authnContextClassRef = $config['authncontextclassref'] ?? null;
94
        $this->SecondFactorMaxAge = $config['secondfactormaxage'] ?? -1;
95
96
        if (array_key_exists('use_inflow_registration', $moduleConfig['registration'])) {
97
            $this->stateData->useInflowRegistration = $moduleConfig['registration']['use_inflow_registration'];
98
        } else {
99
            $this->stateData->useInflowRegistration = true;
100
        }
101
    }
102
103
104
    /**
105
     * Process a authentication response
106
     *
107
     * This function saves the state, and redirects the user to the page where
108
     * the user can register or authenticate with his token.
109
     *
110
     * @param array &$state The state of the response.
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