Skip to content

Commit 1188390

Browse files
committed
Add controllers & tests
1 parent e9e94fd commit 1188390

17 files changed

Lines changed: 1735 additions & 396 deletions

lib/Controller/AuthProcess.php

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<?php
2+
3+
namespace SimpleSAML\Module\webauthn\Controller;
4+
5+
use Datetime;
6+
use Exception;
7+
use SimpleSAML\Auth;
8+
use SimpleSAML\Configuration;
9+
use SimpleSAML\Error;
10+
use SimpleSAML\HTTP\RunnableResponse;
11+
use SimpleSAML\Logger;
12+
use SimpleSAML\Module;
13+
use SimpleSAML\Module\webauthn\WebAuthn\WebAuthnAbstractEvent;
14+
use SimpleSAML\Module\webauthn\WebAuthn\WebAuthnAuthenticationEvent;
15+
use SimpleSAML\Session;
16+
use SimpleSAML\Utils;
17+
use SimpleSAML\XHTML\Template;
18+
use Symfony\Component\HttpFoundation\Request;
19+
use Symfony\Component\HttpFoundation\RedirectResponse;
20+
use Symfony\Component\HttpFoundation\Response;
21+
use Symfony\Component\HttpFoundation\StreamedResponse;
22+
23+
/**
24+
* Controller class for the webauthn module.
25+
*
26+
* This class serves the different views available in the module.
27+
*
28+
* @package SimpleSAML\Module\webauthn
29+
*/
30+
class AuthProcess
31+
{
32+
/** @var \SimpleSAML\Configuration */
33+
protected $config;
34+
35+
/** @var \SimpleSAML\Session */
36+
protected $session;
37+
38+
/**
39+
* @var \SimpleSAML\Auth\State|string
40+
* @psalm-var \SimpleSAML\Auth\State|class-string
41+
*/
42+
protected $authState = Auth\State::class;
43+
44+
/**
45+
* @var \SimpleSAML\Logger|string
46+
* @psalm-var \SimpleSAML\Logger|class-string
47+
*/
48+
protected $logger = Logger::class;
49+
50+
51+
/**
52+
* Controller constructor.
53+
*
54+
* It initializes the global configuration and session for the controllers implemented here.
55+
*
56+
* @param \SimpleSAML\Configuration $config The configuration to use by the controllers.
57+
* @param \SimpleSAML\Session $session The session to use by the controllers.
58+
*
59+
* @throws \Exception
60+
*/
61+
public function __construct(
62+
Configuration $config,
63+
Session $session
64+
) {
65+
$this->config = $config;
66+
$this->session = $session;
67+
}
68+
69+
70+
/**
71+
* Inject the \SimpleSAML\Auth\State dependency.
72+
*
73+
* @param \SimpleSAML\Auth\State $authState
74+
*/
75+
public function setAuthState(Auth\State $authState): void
76+
{
77+
$this->authState = $authState;
78+
}
79+
80+
81+
/**
82+
* Inject the \SimpleSAML\Logger dependency.
83+
*
84+
* @param \SimpleSAML\Logger $logger
85+
*/
86+
public function setLogger(Logger $logger): void
87+
{
88+
$this->logger = $logger;
89+
}
90+
91+
92+
/**
93+
* @param \Symfony\Component\HttpFoundation\Request $request
94+
* @return (\Symfony\Component\HttpFoundation\RedirectResponse|
95+
* \SimpleSAML\HTTP\RunnableResponse|
96+
* \Symfony\Component\HttpFoundation\StreamedResponse) A Symfony Response-object.
97+
*/
98+
public function main(Request $request): Response
99+
{
100+
$this->logger::info('FIDO2 - Accessing WebAuthn enrollment validation');
101+
102+
$stateId = $request->query->get('StateId');
103+
if ($stateId === null) {
104+
throw new Error\BadRequest('Missing required StateId query parameter.');
105+
}
106+
107+
$moduleConfig = Configuration::getOptionalConfig('module_webauthn.php');
108+
$debugEnabled = $moduleConfig->getBoolean('debug', false);
109+
110+
/** @var array $state */
111+
$state = $this->authState::loadState($stateId, 'webauthn:request');
112+
113+
$incomingID = bin2hex(WebAuthnAbstractEvent::base64urlDecode($request->request->get('response_id')));
114+
115+
/**
116+
* §7.2 STEP 2 - 4 : check that the credential is one of those the particular user owns
117+
*/
118+
$publicKey = false;
119+
$previousCounter = -1;
120+
121+
foreach ($state['FIDO2Tokens'] as $oneToken) {
122+
if ($oneToken[0] == $incomingID) {
123+
// Credential ID is eligible for user $state['FIDO2Username'];
124+
// using publicKey $oneToken[1] with current counter value $oneToken[2]
125+
$publicKey = $oneToken[1];
126+
$previousCounter = $oneToken[2];
127+
break;
128+
}
129+
}
130+
131+
if ($publicKey === false) {
132+
throw new Exception(
133+
"User attempted to authenticate with an unknown credential ID. This should already have been prevented by the browser!"
134+
);
135+
}
136+
137+
/** @psalm-var array $oneToken */
138+
$authObject = new WebAuthnAuthenticationEvent(
139+
$request->request->get('type'),
140+
($state['FIDO2Scope'] === null ? $state['FIDO2DerivedScope'] : $state['FIDO2Scope']),
141+
$state['FIDO2SignupChallenge'],
142+
$state['IdPMetadata']['entityid'],
143+
base64_decode($request->request->get('authenticator_data')),
144+
base64_decode($request->request->get('client_data_raw')),
145+
$oneToken[0],
146+
$oneToken[1],
147+
base64_decode($request->request->get('signature')),
148+
$debugEnabled
149+
);
150+
151+
/**
152+
* §7.2 STEP 18 : detect physical object cloning on the token
153+
*/
154+
$counter = $authObject->getCounter();
155+
if (($previousCounter != 0 || $counter != 0) && $counter > $previousCounter) {
156+
// Signature counter was incremented compared to last time, good
157+
$store = $state['webauthn:store'];
158+
$store->updateSignCount($oneToken[0], $counter);
159+
} else {
160+
throw new Exception(
161+
"Signature counter less or equal to a previous authentication! Token cloning likely (old: $previousCounter, new: $counter."
162+
);
163+
}
164+
165+
// THAT'S IT. The user authenticated successfully. Remember the credential ID that was used.
166+
$state['FIDO2AuthSuccessful'] = $oneToken[0];
167+
168+
// See if he wants to hang around for token management operations
169+
if ($request->request->get('credentialChange') === 'on') {
170+
$state['FIDO2WantsRegister'] = true;
171+
} else {
172+
$state['FIDO2WantsRegister'] = false;
173+
}
174+
175+
$this->authState::saveState($state, 'webauthn:request');
176+
177+
if ($debugEnabled) {
178+
$response = new StreamedResponse();
179+
$response->setCallback(function ($authObject, $state) {
180+
echo $authObject->getDebugBuffer();
181+
echo $authObject->getValidateBuffer();
182+
echo "Debug mode, not continuing to " . ($state['FIDO2WantsRegister'] ? "credential registration page." : "destination.");
183+
});
184+
} else {
185+
if ($state['FIDO2WantsRegister']) {
186+
$response = new RedirectResponse(Module::getModuleURL('webauthn.php/webauthn?StateId=' . urlencode($stateId)));
187+
} else {
188+
$response = new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]);
189+
}
190+
}
191+
192+
$response->headers->set('Expires', 'Thu, 19 Nov 1981 08:52:00 GMT');
193+
$response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
194+
$response->headers->set('Pragma', 'no-cache');
195+
196+
/** Symfony 5 style */
197+
/**
198+
$response->setCache([
199+
'must_revalidate' => true,
200+
'no_cache' => true,
201+
'no_store' => true,
202+
'no_transform' => false,
203+
'public' => false,
204+
'private' => false,
205+
]);
206+
$response->setExpires(new DateTime('Thu, 19 Nov 1981 08:52:00 GMT'));
207+
*/
208+
209+
return $response;
210+
}
211+
}

lib/Controller/ManageToken.php

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
namespace SimpleSAML\Module\webauthn\Controller;
4+
5+
use Datetime;
6+
use Exception;
7+
use SimpleSAML\Auth;
8+
use SimpleSAML\Configuration;
9+
use SimpleSAML\Error;
10+
use SimpleSAML\HTTP\RunnableResponse;
11+
use SimpleSAML\Logger;
12+
use SimpleSAML\Module;
13+
use SimpleSAML\Module\webauthn\WebAuthn\StaticProcessHelper;
14+
use SimpleSAML\Session;
15+
use SimpleSAML\Utils;
16+
use Symfony\Component\HttpFoundation\Request;
17+
18+
/**
19+
* Controller class for the webauthn module.
20+
*
21+
* This class serves the different views available in the module.
22+
*
23+
* @package SimpleSAML\Module\webauthn
24+
*/
25+
class ManageToken
26+
{
27+
/** @var \SimpleSAML\Configuration */
28+
protected $config;
29+
30+
/** @var \SimpleSAML\Session */
31+
protected $session;
32+
33+
/**
34+
* @var \SimpleSAML\Auth\State|string
35+
* @psalm-var \SimpleSAML\Auth\State|class-string
36+
*/
37+
protected $authState = Auth\State::class;
38+
39+
/**
40+
* @var \SimpleSAML\Logger|string
41+
* @psalm-var \SimpleSAML\Logger|class-string
42+
*/
43+
protected $logger = Logger::class;
44+
45+
46+
/**
47+
* Controller constructor.
48+
*
49+
* It initializes the global configuration and session for the controllers implemented here.
50+
*
51+
* @param \SimpleSAML\Configuration $config The configuration to use by the controllers.
52+
* @param \SimpleSAML\Session $session The session to use by the controllers.
53+
*
54+
* @throws \Exception
55+
*/
56+
public function __construct(
57+
Configuration $config,
58+
Session $session
59+
) {
60+
$this->config = $config;
61+
$this->session = $session;
62+
}
63+
64+
65+
/**
66+
* Inject the \SimpleSAML\Auth\State dependency.
67+
*
68+
* @param \SimpleSAML\Auth\State $authState
69+
*/
70+
public function setAuthState(Auth\State $authState): void
71+
{
72+
$this->authState = $authState;
73+
}
74+
75+
76+
/**
77+
* Inject the \SimpleSAML\Logger dependency.
78+
*
79+
* @param \SimpleSAML\Logger $logger
80+
*/
81+
public function setLogger(Logger $logger): void
82+
{
83+
$this->logger = $logger;
84+
}
85+
86+
87+
/**
88+
* @param \Symfony\Component\HttpFoundation\Request $request
89+
* @return \SimpleSAML\HTTP\RunnableResponse A Symfony Response-object.
90+
*/
91+
public function main(Request $request): RunnableResponse
92+
{
93+
$this->logger::info('FIDO2 - Accessing WebAuthn token management');
94+
95+
$stateId = $request->query->get('StateId');
96+
if ($stateId === null) {
97+
throw new Error\BadRequest('Missing required StateId query parameter.');
98+
}
99+
100+
/** @var array $state */
101+
$state = $this->authState::loadState($stateId, 'webauthn:request');
102+
103+
if ($state['FIDO2AuthSuccessful'] === false) {
104+
throw new Exception("Attempt to access the token management page unauthenticated.");
105+
}
106+
107+
switch ($request->request->get('submit')) {
108+
case "NEVERMIND":
109+
$response = new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]);
110+
break;
111+
case "DELETE":
112+
$credId = $request->request->get('credId');
113+
if ($state['FIDO2AuthSuccessful'] == $credId) {
114+
throw new Exception("Attempt to delete the currently used credential despite UI preventing this.");
115+
}
116+
117+
$store = $state['webauthn:store'];
118+
$store->deleteTokenData($credId);
119+
120+
if (array_key_exists('Registration', $state)) {
121+
foreach ($state['FIDO2Tokens'] as $key => $value) {
122+
if ($state['FIDO2Tokens'][$key][0] == $credId) {
123+
unset($state['FIDO2Tokens'][$key]);
124+
break;
125+
}
126+
}
127+
128+
$response = new RunnableResponse([StaticProcessHelper::class, 'saveStateAndRedirect'], [$state]);
129+
} else {
130+
$response = new RunnableResponse([Auth\ProcessingChain::class, 'resumeProcessing'], [$state]);
131+
}
132+
break;
133+
default:
134+
throw new Exception("Unknown submit button state.");
135+
}
136+
137+
$response->headers->set('Expires', 'Thu, 19 Nov 1981 08:52:00 GMT');
138+
$response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
139+
$response->headers->set('Pragma', 'no-cache');
140+
141+
/** Symfony 5 style */
142+
/**
143+
$response->setCache([
144+
'must_revalidate' => true,
145+
'no_cache' => true,
146+
'no_store' => true,
147+
'no_transform' => false,
148+
'public' => false,
149+
'private' => false,
150+
]);
151+
$response->setExpires(new DateTime('Thu, 19 Nov 1981 08:52:00 GMT'));
152+
*/
153+
154+
return $response;
155+
}
156+
}

0 commit comments

Comments
 (0)