안녕하세요. 이전시간에는 asbiin/laravel-webauthn 패키지를 자신의 프로젝트에 도입하는 실습을 해보았는데요. 오늘은 이 패키지가 어떻게 이루어져있고 어떤 Flow로 등록이 되는지 탑다운 방식으로 파헤쳐보겠습니다.
먼저 resources/vendor/webauthn/register.blade.php 를 보겠습니다. HTML부분은 자신이 원하는 디자인에 맞게 커스텀해주시면 될 것 같고 유심히 볼 것은 <form>태그와 <script>부분입니다. 먼저 <script>부분을 보도록 하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<script>
var publicKey = {!! json_encode($publicKey) !!};
...
var webauthn = new WebAuthn((name, message) => {
error(errorMessage(name, message));
});
...
webauthn.register(
publicKey,
function (datas) {
$('#success').removeClass('d-none');
$('#register').val(JSON.stringify(datas)),
$('#form').submit();
}
);
</script>
|
cs |
에러를 처리하기 위한 callback function을 생성자 인자로 넣어주고 asbiin/laravel-webauthn 패키지에 있는 WebAuthn 객체를 생성하는 모습입니다. 이 내용은 public/vendor/webauthn.js 에 정의되어있습니다. register()함수를 확인해보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
WebAuthn.prototype.register = function(publicKey, callback) {
let publicKeyCredential = Object.assign({}, publicKey);
publicKeyCredential.user.id = this._bufferDecode(publicKey.user.id);
publicKeyCredential.challenge = this._bufferDecode(this._base64Decode(publicKey.challenge));
if (publicKey.excludeCredentials) {
publicKeyCredential.excludeCredentials = this._credentialDecode(publicKey.excludeCredentials);
}
var self = this;
navigator.credentials.create({
publicKey: publicKeyCredential
}).then((data) => {
self._registerCallback(data, callback);
}, (error) => {
self._notify(error.name, error.message, false);
});
}
|
cs |
아... 드디어 여기에 W3C의 navigator.credentials.create()가 정의되어있는 모습입니다. 그런데 여기서 publicKeyCredential, Fido 스펙의 5.4. Options for Credential Creation (dictionary PublicKeyCredentialCreationOptions)가 위의 스크립트에서 인자로 받아 Public Key를 생성하는데 이를 서버에서 받아오도록 작성되어있습니다. ( var publicKey = {!! json_encode($publicKey) !!} ) 음 .. 그렇다면 /webauthn/register라우트의 컨트롤러 함수를 보아야 겠죠?
라우트는 패키지 디렉토리인 vendor/asbiin/laravel-webauthn/WebauthnServiceProvider 의 registerRoutes()에 정의되어있습니다.
1
2
3
4
5
6
7
8
9
10
11
|
private function registerRoutes()
{
Route::group($this->routeConfiguration(), function (\Illuminate\Routing\Router $router) : void {
$router->get('auth', 'WebauthnController@login')->name('webauthn.login');
$router->post('auth', 'WebauthnController@auth')->name('webauthn.auth');
$router->get('register', 'WebauthnController@register')->name('webauthn.register');
$router->post('register', 'WebauthnController@create')->name('webauthn.create');
$router->delete('{id}', 'WebauthnController@destroy')->name('webauthn.destroy');
});
}
|
cs |
(GET) route(webauthn.register)가 WebauthnController의 register() 메소드에 매핑되어있는 것을 확인할 수 있습니다. 메소드를 확인해 보겠습니다. 컨트롤러는 vendor/asbiin/laravel-webauthn/Http/Controllers/WebauthnController 에 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
use LaravelWebauthn\Facades\Webauthn;
...
public function register(Request $request)
{
$publicKey = Webauthn::getRegisterData($request->user());
$request->session()->put(self::SESSION_PUBLICKEY_CREATION, $publicKey);
return $this->redirectViewRegister($request, $publicKey);
}
protected function redirectViewRegister(Request $request, PublicKeyCredentialCreationOptions $publicKey)
{
if ($this->config->get('webauthn.register.view', '') !== '') {
return view($this->config->get('webauthn.register.view'))
->withPublicKey($publicKey)
->withName($request->input('name'));
} else {
return Response::json([
'publicKey' => $publicKey,
]);
}
}
|
cs |
음 .. 여기서 같은 패키지에 있는 파사드인 Webauthn 객체를 가져오는 군요.. publicKey를 이곳에서 생성하니 한번 보아야겠습니다. 이 파사드는 vendor/asbiin/laravel-webauthn/Http/Facades/Webauthn 에 있습니다.
1
2
3
4
5
6
7
|
class Webauthn extends Facade
{
protected static function getFacadeAccessor()
{
return \LaravelWebauthn\Services\Webauthn::class;
}
}
|
cs |
당연하게도 서비스 객체를 리턴하고 있으니 이 객체도 살펴보아야겠습니다.
vendor/asbiin/laravel-webauthn/Http/Services/Webauthn 에 정의되어있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class Webauthn extends WebauthnRepository
{
// ...
public function getRegisterData(User $user) : PublicKeyCredentialCreationOptions
{
$publicKey = $this->app->make(PublicKeyCredentialCreationOptionsFactory::class)
->create($user);
$this->events->dispatch(new WebauthnRegisterData($user, $publicKey));
return $publicKey;
}
// ...
}
|
cs |
$publicKey를 PublicKeyCredentialCreationOptions에서 만들어서 가져오고있습니다. 이파일도 확인해야겠군요 ㅠㅠ
vendor/asbiin/laravel-webauthn/Http/Services/Webauthn 에 정의되어있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
final class PublicKeyCredentialCreationOptionsFactory extends AbstractOptionsFactory
{
/**
* Create a new PublicKeyCredentialCreationOptions object.
*
* @param User $user
* @return PublicKeyCredentialCreationOptions
*/
public function create(User $user): PublicKeyCredentialCreationOptions
{
$userEntity = new PublicKeyCredentialUserEntity(
$user->email ?: '',
$user->getAuthIdentifier(),
$user->email ?: '',
null
);
return new PublicKeyCredentialCreationOptions(
$this->createRpEntity(),
$userEntity,
random_bytes($this->config->get('webauthn.challenge_length', 32)),
$this->createCredentialParameters(),
$this->config->get('webauthn.timeout', 60000),
$this->repository->getRegisteredKeys($user),
$this->createAuthenticatorSelectionCriteria(),
$this->config->get('webauthn.attestation_conveyance') ?: PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
$this->createExtensions()
);
}
private function createAuthenticatorSelectionCriteria(): AuthenticatorSelectionCriteria
{
return new AuthenticatorSelectionCriteria(
$this->config->get('webauthn.authenticator_selection_criteria.attachment_mode') ?: AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
$this->config->get('webauthn.authenticator_selection_criteria.require_resident_key', false),
$this->config->get('webauthn.authenticator_selection_criteria.user_verification') ?: AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED
);
}
private function createRpEntity(): PublicKeyCredentialRpEntity
{
return new PublicKeyCredentialRpEntity(
$this->config->get('app.name', 'Laravel'),
Request::getHttpHost(),
$this->config->get('webauthn.icon')
);
}
/**
* @return PublicKeyCredentialParameters[]
*/
private function createCredentialParameters(): array
{
$callback = function ($algorithm) {
return new PublicKeyCredentialParameters(
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
$algorithm
);
};
return array_map($callback, $this->config->get('public_key_credential_parameters') ?: [
\Cose\Algorithms::COSE_ALGORITHM_ES256,
\Cose\Algorithms::COSE_ALGORITHM_RS256,
]);
}
}
|
cs |
이걸 보니 웬만한 설정은 config/webauthn 에서 설정이 가능한가봅니다..? ( ㅡㅡ;; Document에 자세하게 좀 써주지 .. ) 여기에서 특이한 것이 하나 있는데 create() 메소드의 리턴값이 PublicKeyCredentialCreationOptions객체인데 PublicKeyCredentialOptions을 상속하고있으며 이 두 파일은 각각 아래와 같이 있습니다.
- vendor/web-authn/webauthn-lib/src/PublicKeyCredentialCreationOptions
- vendor/web-authn/webauthn-lib/src/PublicKeyCredentailOptions
Fido2 Webauthn 예제를 보면 PublicKeyCredentialCreationOption의 challenge값을 그냥 Client-Side에서 생성하는 것도 있던데 Fido Spec에 따르면 서버에서 생성해서 주는 것이 맞는 것 같습니다. Fido Spec Cryptographic Challenges
자 이제 패키지를 통해 navigator.credentials.create()에 필요한 인자값을 전달하는 Flow와 커스터마이징 방법을 알아보았습니다. (Fido 서버 분리하면 PublicKeyCredentialCreationOption생성 부분만 따로 라우트로빼서 User객체 던져주고 받으면 되겠네요) 다음 포스트에서는 Validation과 실제 등록 과정을 살펴보겠습니다.