본문 바로가기

Fido2/WebAuthn

라라벨 프로젝트에 WebAuthn 도입하기 (2) 등록 Flow 1 (Registration Flow) PublicKeyCredentialCreationOptions

안녕하세요. 이전시간에는 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과 실제 등록 과정을 살펴보겠습니다.