ロゴテキスト ロゴ

    パスワード強度のお手軽表示!zxcvbn-tsをReactに導入してみる

    パスワード強度のお手軽表示!zxcvbn-tsをReactに導入してみる

    パスワードの強度を計測してくれる zxcvbn-ts



    Reactのアプリケーションに試しに入れてみたため、実装方法を記載していきます!




    先に使った感想を記載しておきます

    • 実装めっちゃ簡単。お手軽にパスワードの強度評価してくれる(無料です ☺️)
    • TypeScriptの補完しっかり効きます!

    検証した環境

    1 @zxcvbn-ts/core 3.0.3
    2 @zxcvbn-ts/language-common 3.0.3
    3 @zxcvbn-ts/language-ja 3.0.1
    4 react 18.2.0
    5 next 13.4.4

    zxcvbn-tsの導入

    公式サイトにexampleがしっかり書いてあるのでそちらを参考に。




    まずパッケージをインストール

    yarn add @zxcvbn-ts/core @zxcvbn-ts/language-common @zxcvbn-ts/language-ja

    指定可能な言語はGitHubのページに記載されています

    zxcvbn/packages/languages at master · zxcvbn-ts/zxcvbn




    公式のコードを参考にconsole.logを使って表示出来る事を確認します

    'use client'
     
    import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core'
    import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common'
    import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja'
     
    const SamplePage = () => {
      const password = 'somePassword'
      const options = {
        translations: zxcvbnJaPackage.translations,
        graphs: zxcvbnCommonPackage.adjacencyGraphs,
        dictionary: {
          ...zxcvbnCommonPackage.dictionary,
          ...zxcvbnJaPackage.dictionary,
        },
      }
     
      zxcvbnOptions.setOptions(options)
      const result = zxcvbn(password)
      console.log('result: ', result)
     
      // zxcvbnを試したいため画面には何も表示しない
      return <div />
    }
     
    export default SamplePage

    ↑はNext.jsのApp Routerを使ったコードになっています。

    状況に合わせて読み替えて下さいmm





    console.logの中を見てみると


    日本語のfeedbackが入ってる…!


    その他にも表示に役立ちそうな値が色々入っていそうです。



    zxcvbn-tsの返り値の確認

    zxcvbnが返す値の詳細は公式のOutputという項目にしっかり記載されています。

    重要そうなものを3つピックアップします。

    score

    パスワードの安全性を計る指標。


    0 から 4 の値が返ってくる5段階評価で、0だと安全性が低く4だと安全性が高くなります




    公式の記載内容をDeepLで翻訳すると以下のようになります。日本語が・・

    • 0・・推測可能すぎる : 危険なパスワード
    • 1・・非常に推測しやすい : オンライン攻撃からの保護
    • 2・・ある程度推測できる : スロットルのないオンライン攻撃からの保護
    • 3・・安全無比 : オフライン・スローハッシュ・シナリオからの適度な保護
    • 4・・推し量れない : オフライン・スローハッシュ・シナリオからの強力な保護


    crackTimesSeconds

    秒で数えた際の推定クラック時間、以下のような値が返ってきます。

    {
        "onlineThrottling100PerHour": 18734400,
        "onlineNoThrottling10PerSecond": 52040,
        "offlineSlowHashing1e4PerSecond": 52.04,
        "offlineFastHashing1e10PerSecond": 0.00005204
    }

    再度DeepL翻訳。下2つは何言ってるか分からない・・😂

    • onlineThrottling100PerHour・・パスワード認証の試行を制限するサービスへのオンライン攻撃
    • onlineNoThrottling10PerSecond・・レートを制限しないサービスへのオンライン攻撃
    • offlineSlowHashing1e4PerSecond・・オフライン攻撃。複数の攻撃者を想定、ユーザー固有の適切なソルティングと低速のハッシュ関数が必要。bcrypt、scrypt、PBKDF2のような、適度な仕事率を持つハッシュ関数。
    • offlineFastHashing1e10PerSecond・・ユーザー固有のソルティングを用いたオフライン攻撃。SHA-1、SHA-256またはMD5のような高速ハッシュ関数。幅広い範囲10億から1兆までの妥当な数コア数とマシンによる。10B/秒を目安に。


    feedback

    入力されたパスワードに対してのフィードバック。

    画面上に表示出来るような文字列を返してくれます。



    password の場合(scoreは0)

    feedback: {
      warning: '一般的に使用されるパスワードです',
      suggestions: [ '一般的ではない単語を増やしてください' ]
    }

    somePassword の場合(scoreは1)

    feedback: {
      warning: 'よく使われるパスワードに似ています',
      suggestions: [ '一般的ではない単語を増やしてください', '最初の文字以外も大文字にしてください' ]
    }

    c2DL6J8b の場合(scoreは2)

    feedback: {
      warning: null,
      suggestions: [ '一般的ではない単語を増やしてください' ] 
    }


    試してみる

    パスワード生成ツールを使っていくつか試してみました

    パスワードスコアofflineSlowHashing1e4PerSecondメモ
    a00.0012
    password00.0003
    pV8k11.0001ランダム4桁(記号なし)
    kF4J321100.0001ランダム6桁(記号なし)
    c2DL6J8b210000.0001ランダム8桁(記号なし)
    t9GeU.x_210000.0001ランダム8桁(記号入)
    PYAWykeD7C31000000.0001ランダム10桁(記号なし)
    38Kj(i7cZ!31000000.0001ランダム10桁(記号入)
    VY&TbWU#xMD34100000000.0001ランダム12桁(記号入)
    RbHMsVkiVt3KHjsj41000000000000ランダム16桁(記号なし)

    12桁までくるとかなり強いんですねーー!


    また、この秒数によると桁数が増えれば増えるほど、

    記号を含めなくても強くなるみたいです。

    Reactで強度を表示する

    では本題のReactのコードです!



    以下のような環境での実装になります

    • App Routerを用いたNext.js
    • cssはtailwindを使用
    'use client'
     
    import { useEffect, useState } from 'react'
     
    import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core'
    import { ZxcvbnResult } from '@zxcvbn-ts/core/src/types'
    import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common'
    import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja'
     
    const SamplePage = () => {
      // 入力されたパスワードの値
      const [password, setPassword] = useState<string | undefined>(undefined)
      // zxcvbnの結果
      const [result, setResult] = useState<ZxcvbnResult | undefined>(undefined)
     
      // zxcvbnの設定
      const options = {
        translations: zxcvbnJaPackage.translations,
        graphs: zxcvbnCommonPackage.adjacencyGraphs,
        dictionary: {
          ...zxcvbnCommonPackage.dictionary,
          ...zxcvbnJaPackage.dictionary,
        },
      }
      zxcvbnOptions.setOptions(options)
     
      useEffect(() => {
        // パスワードがない場合はzxcvbnの結果をリセットする
        if (!password) {
          setResult(undefined)
          return
        }
     
        // 入力されたパスワードを用いてzxcvbnの結果を取得、useStateに格納する
        setResult(zxcvbn(password))
      }, [password, result])
     
      return (
        <div className="w-[300px]">
          <div className="grid gap-[20px]">
            <label htmlFor="email">
              <div>email</div>
              <input
                type="text"
                id="email"
                className="h-[30px] w-full rounded-[4px] border border-[#909090]"
              />
            </label>
            <label htmlFor="password">
              <div>password</div>
              <input
                type="text"
                id="password"
                className="h-[30px] w-full rounded-[4px] border border-[#909090]"
                onChange={e => setPassword(e.target.value)}
              />
            </label>
          </div>
     
          <div className="mt-[60px]">
            <div className="flex w-full gap-[1%]">
              {/* 強度を表す5段階のバーを表示 */}
              {[0, 1, 2, 3, 4].map(v => (
                <div
                  className={`h-[4px] w-[24%] ${
                    result && v <= result.score ? 'bg-[#666]' : 'bg-[#ccc]'
                  }`}
                  key={v}
                />
              ))}
            </div>
            {result && result.score + 1}
          </div>
     
          {/* feedback.warning がある場合は表示 */}
          {result?.feedback && <div className="text-[#f00]">{result.feedback.warning}</div>}
        </div>
      )
    }
     
    export default SamplePage



    強度を表示出来ました!🎉




    ざっくりやっている事としては

    • パスワード用のinputの値を useState の password に設定(55行目)
    • 上記値が更新される度に zxcvbn が実行され useState の result が更新される(35行目)
    • result の値を元に結果をHTML上に表示(60 - 76行目)

    となります。




    簡単に強度が表示が出来て、しっかりTypeScriptの補完も効くので

    zxcvbn-ts かなり使いやすい印象でした!



    余談

    そもそも今回zxcvbn-tsを使ってみようと思ったきっかけがこちらです。


    読みやすく分かりやすい、大変ステキな記事です ☺️




    上記記事に書かれている内容としまして

    アプリケーションを作る時、皆さんは新規登録フォームで「パスワード再入力」というinputを用意してませんか?





    私、今まで特に意識をしてなかったのですが、実は

    • コンバージョンが落ちる
    • 再入力の部分はパスワードをコピペするだけで再確認になっていない

    という研究結果があるそうです

    We achieved a 56.3% increase in form conversions by removing “Confirm Password” while not negatively affecting the password reset rate.



    DeepL翻訳: パスワードのリセット率に悪影響を与えることなく、「パスワードの確認」を削除することで、フォームコンバージョンを56.3%増加させることができました。

    フォームで「パスワードの確認」を使用する必要がありますか?: ケーススタディ より引用



    私「パスワード再入力」inputがある新規登録ページの実装何度かしてきたのですが、

    この辺りのUXてこんな考える事が色々あるんですね。。


    いやぁ難しい…!

    プロフィールの背景画像 プロフィール画像
    Yuki Takara
    都内でフリーランスのエンジニアをやってます。フロントとアプリ開発メインに幅広くやってます。