(第1回)WEBシステムをスマホの指紋認証でログインしてみる(Passwordless API)

(第1回)WEBシステムをスマホの指紋認証でログインしてみる(Passwordless API)

目次

概要
使用環境
APIキーの取得
ライブラリ追加
入力画面作成
資格情報の登録
  ①トークンを取得
  ②資格情報を登録
認証
  ①ユーザIDを基に登録済みのトークンを取得
  ②サインインの検証
動かしてみる
最後に

概要

Webシステムをスマートフォンで使用する際に、毎回ログインID、パスワードを手入力するのは大変ですよね。
普段から使用している指紋認証や顔認証でログインできれば使い勝手も良くなり、セキュリティ面でも安心です。

ブラウザ標準で対応しているWebAuthn(Web Authentication)という認証技術を使用し
スマートフォンの指紋認証や顔認証、YubiKeyなどの外部デバイスでの認証が可能となっています。

WebAuthnを全て理解し実装するのは時間がかかり大変ですので
今回は「Passwordless.dev」というサービスを利用し、簡単に指紋認証を使用したログインの実装を目指したいと思います。

第1回では、スマートフォンの指紋認証を使用し、資格情報の登録、認証を行います。

参考
Passwordless guide
passwordless-dotnet-example

使用環境

フレームワーク:.NET 6.0
端末:Android
ブラウザ:Chrome
開発環境:Visual Studio2022

Visual Studio 2022で、「ASP.NET Core Webアプリ(MVC)」のテンプレートで作成したプロジェクトに組み込みます。

APIキーの取得

下記ページにてAPIキーを取得します。
今回は無償版を使用します。
https:// www.passwordless.dev/

「Get started」をクリックし、Account name、Admin emailを入力。「Create API key」をクリックします。
画面にapikey(公開鍵)とapisecret(秘密鍵)が表示されますのでダウンロードして保管してください。

ライブラリ追加

Passwordless APIとのやりとりを行うためのスクリプトタグを追加します。
今回の実装ではテンプレートで生成されているHome/Index.cshtml内に記載しています。

<script src="https://cdn.passwordless.dev/dist/0.2.0/passwordlessclient.iife.min.js" crossorigin="anonymous"></script>

入力画面作成

ユーザID用のテキストボックスとボタンを用意します。
登録、認証用に2つ用意してます。

<div class="row" style="margin-bottom:20px">
    <h2>登録</h2>
    <div class="col-md-4">
        <div class="form-floating">
            <input type="text" class="form-control" id="register_user" placeholder="ユーザID" />
            <label for="register_user">ユーザID</label>
        </div>
        <button type="button" id="passwordless-register" class="w-100 btn btn-lg btn-primary" >Register</button>
    </div>
</div>
<div class="row">
    <h2>サインイン</h2>
    <div class="col-md-4">

        <div class="form-floating">
            <input type="text" class="form-control" id="signin_user" placeholder="ユーザID" />
            <label for="signin_user">ユーザID</label>
        </div>
        <button type="button" id="passwordless-signin" class="w-100 btn btn-lg btn-primary" >SignIn</button>
    </div>
</div>

資格情報の登録

資格情報の登録では以下を行います。
①トークンを取得
②資格情報を登録

①トークンを取得

バックエンドの処理(今回のプロジェクトではController部分)で、
Passwordless APIの”register/token”を呼び出します。

POST
https://apiv2.passwordless.dev/register/token
ヘッダ
ApiSecret: 秘密鍵
Content-Type: application/json
パラメータ
{ "UserId": "", "username": "", "displayName": "" } 
private HttpClient _httpClient;
private readonly static string API_SECRET = "<取得した秘密鍵>";

public FIDOController()
{
    _httpClient = new HttpClient();
    _httpClient.BaseAddress = new Uri("https://apiv2.passwordless.dev/");
    _httpClient.DefaultRequestHeaders.Add("ApiSecret", API_SECRET);
}

public async Task<ActionResult<string>> GetRegisterToken(string user_id)
{
    var json = JsonSerializer.Serialize(new
    {
        userId = user_id,
        username = user_id,
        DisplayName = "Mr Guest"
    });

    var request = await _httpClient.PostAsync("register/token", new StringContent(json, Encoding.UTF8, "application/json"));
    request.EnsureSuccessStatusCode();
    var token = await request.Content.ReadAsStringAsync();

    return token;
}

「FIDOController.cs」を追加し上記を実装します。
※コントローラー名は任意です

private HttpClient _httpClient;
private readonly static string API_SECRET = "<取得した秘密鍵>";

public FIDOController()
{
    _httpClient = new HttpClient();
    _httpClient.BaseAddress = new Uri("https://apiv2.passwordless.dev/");
    _httpClient.DefaultRequestHeaders.Add("ApiSecret", API_SECRET);
}

呼び出すAPIのアドレスBaseAddressに「https://apiv2.passwordless.dev/」を設定。
リクエストのヘッダにAPIキーの取得で取得した秘密鍵を設定します。

上記をクライアント側のスクリプトから呼び出します。

    const API_KEY = "<取得した公開鍵>";

    async function RegisterPasswordless(e) {
        e.preventDefault();

        const userId = $("#register_user").val();

        //公開鍵でPasswordless clientを起動
        const p = new Passwordless.Client({
          apiKey: API_KEY,
        });

        //バックエンドを呼び出してトークンを取得
        var url = "@Url.Action("GetRegisterToken","FIDO")";
        const myToken = await fetch(url + "?user_id=" + userId).then((r) => r.text());
    }

②資格情報を登録

①で取得したトークンを使用し、登録を行います。

    const API_KEY = "<取得した公開鍵>";

    async function RegisterPasswordless(e) {
        e.preventDefault();

        const userId = $("#register_user").val();

        //公開鍵でPasswordless clientを起動
        const p = new Passwordless.Client({
          apiKey: API_KEY,
        });

        //バックエンドを呼び出してトークンを取得
        var url = "@Url.Action("GetRegisterToken","FIDO")";
        const myToken = await fetch(url + "?user_id=" + userId).then((r) => r.text());

		//トークンを使用し登録
        try {
          await p.register(myToken);

        } catch (e) {
          console.error("Things went bad", e);
        }
    }

認証

認証では以下を行います。
①ユーザIDを基に登録済みのトークンを取得
②サインインの検証

①ユーザIDを基に登録済みのトークンを取得

クライアント側でユーザIDを基にトークンを取得し、サインインを開始します。

    async function handleSignInSubmit(e) {
        e.preventDefault();
        const user_id = $("#signin_user").val();

        //公開APIキーでPasswordless clientを起動
        const p = new Passwordless.Client({
          apiKey: API_KEY,
        });

        try {
            //パスワードレスAPIとブラウザがユーザIDに基づきサインインを開始します
            const token = await p.signinWithId(user_id);
        } catch (e) {
            console.error("Things went really bad: ", e);
        }
    }

②サインインの検証

バックエンドの処理で、Passwordless APIの”signin/verify”を呼び出します。

POST
https://apiv2.passwordless.dev/signin/verify
ヘッダ
ApiSecret: 秘密鍵
Content-Type: application/json
パラメータ
{ "token": "yUf6_wWdDh02ItIvnCKT_02ItIvn…" }
public async Task<IActionResult> VerifySignInToken(string token)
{
    // fido2認証が有効かどうか、どのユーザーに対して有効かを確認
    var json = JsonSerializer.Serialize(new
    {
        token
    });

    var request = await _httpClient.PostAsync("signin/verify", new StringContent(json, Encoding.UTF8, "application/json"));
    request.EnsureSuccessStatusCode();
    var response = await request.Content.ReadAsStringAsync();
    var signin = JsonSerializer.Deserialize<SignInDto>(response, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

    return Json(signin);
}

public class SignInDto
{
    public bool Success { get; set; }

    public string UserId { get; set; }
    public DateTime Timestamp { get; set; }
    public string RPID { get; set; }
    public string Origin { get; set; }
    public string Device { get; set; }
    public string Country { get; set; }
    public string Nickname { get; set; }
    public DateTime ExpiresAt { get; set; }
}

上記をクライアント側のスクリプトから呼び出します。

    async function handleSignInSubmit(e) {
        e.preventDefault();
        const user_id = $("#signin_user").val();

        //公開APIキーでPasswordless clientを起動
        const p = new Passwordless.Client({
          apiKey: API_KEY,
        });

        try {
            //パスワードレスAPIとブラウザがエイリアスに基づきサインインを開始します
            const token = await p.signinWithId(user_id);

            //バックエンドを呼び出して、サインインから作成されたトークンを検証
            var url = "@Url.Action("VerifySignInToken","FIDO")";
            const user = await fetch(url + "?token=" + token).then((r) =>
                r.json()
            );
        } catch (e) {
            console.error("Things went really bad: ", e);
        }
    }

動かしてみる

以上で実装が完了したので発行してサーバーに配置します。
実行時の注意点として、WebAuthnはlocalhostまたはhttpsでないと正しく認証できません

スマートフォンで作成したページにアクセスしてみます。

無事表示されました。
状況が分かるように、右側にログを表示するようにしています。

次は登録してみます。
登録のユーザIDに「user01」を入力し、「Register」ボタンをクリック

スマートフォンの指紋認証が表示されました!
(指紋認証の画面はスクリーンショットが取れないのでカメラで撮影)

認証してみると。。

小さいですが「Successfully registered」が表示され、登録成功です。

登録したユーザでサインインしてみます。
サインインのユーザIDに「user01」を入力し「SignIn」ボタンをクリック

登録時と同じように指紋認証が表示されるので認証。

認証成功です!
ログのUser detailsに情報が表示されています。

最後に

ライブラリを使用したため細かいカスタマイズができませんが、簡単に実装が可能です。

今回はWebAuthnでの登録、認証のみなので、これだけではWEBシステムにログインができません。
次回は、ID、PWで登録したユーザが存在するWEBシステムに今回の内容を組み込み、
既存ユーザとの紐づけ、WEBシステムへのログインを実装してみたいと思います。