ログイン


Important points when using this code

  1. If using WireGuard VPN, make sure you exclude LAN traffic - otherwise, QR/cross device will not work (timesout) - ask me!
  2. Assumes use of CakePHP5 and PHP8, Authentication plugin (3.x)
  3. This demo site is 216.230.241.221 - it is Biznomos dev server. DocumentRoot is /var/www/passkey.dreamersi.net
  4. SVN repo URL is: svn://svn.pspinc.com/prj/cakephp5-passkey/trunk
  5. Use HTTPS. Passkeys won't work without it
  6. Create the following table and bake a model for it
        CREATE TABLE `user_passkeys` (
          `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
          `user_id` int(10) unsigned NOT NULL,
          `credential_id` varbinary(255) NOT NULL,
          `public_key` text NOT NULL,
          `aaguid` varbinary(16) DEFAULT NULL,
          `sign_count` int(11) NOT NULL,
          `created` datetime NOT NULL,
          `modified` datetime NOT NULL,
          `last_used` datetime DEFAULT NULL,
          PRIMARY KEY (`id`),
          UNIQUE KEY `cred_idx` (`credential_id`)
        );
    
  7. (Recommended) instead of copying the vendor directory in this repo, install lbuchs/webauthn like this:
        cd /var/www/
        composer require lbuchs/webauthn
    
  8. Edit your routes - add the following
        $routes->scope('/passkeys', function ($routes) {
            $routes->connect('/login-options',    ['controller' => 'Passkeys', 'action' => 'loginOptions'])->setMethods(['POST']);
            $routes->connect('/login-finish',     ['controller' => 'Passkeys', 'action' => 'loginFinish'])->setMethods(['POST']);
            $routes->connect('/register-options', ['controller' => 'Passkeys', 'action' => 'registerOptions'])->setMethods(['POST']);
            $routes->connect('/register-finish',  ['controller' => 'Passkeys', 'action' => 'registerFinish'])->setMethods(['POST']);
        });
    
        $routes->scope('/', function (RouteBuilder $builder): void {
            // First one is optional
            $builder->connect('/',               ['controller' => 'Users', 'action' => 'login'])->setMethods(['GET', 'POST']);
    
            // Make sure the next two matches loginUrl in src/Application.php
            $builder->connect('/login',          ['controller' => 'Users', 'action' => 'login'])->setMethods(['GET', 'POST']);
            $builder->connect('/login-via-ajax', ['controller' => 'Users', 'action' => 'loginViaAjax'])->setMethods(['POST']);
    
  9. Configure constants - add these to your app_local.php:
        'PASSKEY_RP_NAME'   => 'Name of your service',
        'PASSKEY_RP_ID'     => 'foobar.dreamersi.net',
        'PASSKEY_RP_ORIGIN' => 'https://foobar.dreamersi.net'
    
  10. In your src/Application.php, allow multiple login URLs like this:
        $authenticationService->loadAuthenticator('Authentication.Form', [
            'fields' => [
                'username' => 'email',
                'password' => 'password',
            ],
            'loginUrl' => ['/login', '/login-via-ajax']
        ]);
    
  11. In your layout file, set the CSRF token like this (it is read by JavaScript)
        echo $this->Html->meta('csrfToken', $this->request->getAttribute('csrfToken'));
    
  12. Copy the JS files - they are in webroot/js/passkeys
  13. Copy the PasskeysController.php and use it in your code
  14. Copy the loginViaAjax action in UsersController.php and place it in your UsersController.php
  15. (For PHP7.4 only) Add this to your config/bootstrap.php - it adds a function missing in PHP7.4 that is required by webauthn
        // Polyphil - for PHP7.4
        if (!function_exists('str_starts_with')) {
            function str_starts_with($haystack, $needle) {
                return strncmp($haystack, $needle, strlen($needle)) === 0;
            }
        }
    
  16. Copy passkeys.po in resources/locales/ja_JP and en_US to your folder - these are required to translate from aaguid to provider names
  17. In your UsersController.php, exempt loginViaAjax from form protection like this:
        public function beforeFilter(\Cake\Event\EventInterface $event)
        {
            parent::beforeFilter($event);
            $this->Authentication->addUnauthenticatedActions(['login', 'logout', 'loginViaAjax']);
    
            $this->FormProtection->setConfig('unlockedActions', [
                'loginViaAjax',
            ]);
        }
    
  18. Note: credential_id and aaguid are VARBINARY and will be hydrated as stream resources. Use stream_get_contents() when needed.
        $aaguidBinary = stream_get_contents($passkey->aaguid);
        $aaguidHex    = bin2hex($aaguidBinary);
        $deviceType   = strtolower($aaguidHex);
    
        // Then you can load SVG images like this - see Users/manage_passkeys.php
        $imagePath = '/img/passkeys/' . $deviceType . '.svg';
     
        // Also get the provider name (Google, Apple etc) like this:
        $label     = __d('passkeys', $deviceType); // actual names are stored in translation files