なつねこメモ

主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ 書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。

hanko.io の Flow API を使ってユーザー登録を実現したい

hanko.io という ID プロバイダーがある。 Passkey ネイティブ対応で、 Passkey の他に SNS アカウントでの認証 (X はない)、 OTP での認証など様々な認証形式をサポートしている。比較的最近 MFA にも対応していた。 そんな Hanko だが、 Hanko による認証は hanko-elements を使う方法 (公式の Quickstart はこっち) と API を自分で叩く方法の2種類ある。

今回は後者の方法を使って、アカウントの登録までをやってみる。 API ドキュメントがあまりにまとめすぎて微妙に不親切なので迷いやすい。 API といっても、 Hanko 1.0 以前から使えた API での認証方法と、 1.0 から追加された Flow API の2つがあり、現在は Flow API が推奨されている。最近個人サービスの認証を徐々に Flow API に移しつつあるので、今回は Hanko Cloud で Flow API を使ってのアカウント登録を説明する。

まずは、自身のテナントドメイン、もしくは設定したドメインの /registration に対して空リクエストを送信する。

const preflightResponse= await fetch(`https://${YOUR_HANKO_DOMAIN}/registration`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  }
});
const preflightResponseJson= await preflightResponse.json();

すると、次のようなレスポンスが返ってくる:

{
  "name": "preflight",
  "status": 200,
  "payload": {},
  "csrf_token": "CSRF_TOKEN",
  "actions": {
    "register_client_capabilities": {
      "href": "/registration?action=register_client_capabilities%40SESSION_ID",
      "inputs": {
        "webauthn_available": {
          "name": "webauthn_available",
          "type": "boolean",
          "required": true,
          "hidden": true
        },
        "webauthn_conditional_mediation_available": {
          "name": "webauthn_conditional_mediation_available",
          "type": "boolean",
          "hidden": true
        },
        "webauthn_platform_authenticator_available": {
          "name": "webauthn_platform_authenticator_available",
          "type": "boolean",
          "hidden": true
        }
      },
      "action": "register_client_capabilities",
      "description": "Send the computers capabilities."
    }
  },
  "links": null
}

次にやることは、このレスポンスの actions.register_client_capabilities.href に対して、 inputs で指定された値を送信すること。 どうにかして webauthn_available の値を埋めて、次のようにリクエストを投げれば良い:

const { csrf_token, actions } = preflightResponseJson;
const { register_login_identifier } = actions;
const { href } = register_login_identifier;

// 一旦仮で webauthn_available は false として打ち返す。
const registerClientCapabilitiesResponse = await fetch(`https://${YOUR_HANKO_DOMAIN}/${href}`, {
  method: "POST",
  heders: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    csrf_token,
    input_data: {
      webauthn_available: false,
    },
  }),
});
const registerClientCapabilitiesResponseJson = await registerClientCapabilitiesResponse.json();

今度得られる JSON は次の通り:

{
  "name": "registration_init",
  "status": 200,
  "payload": {},
  "csrf_token": "CSRF_TOKEN",
  "actions": {
    "register_login_identifier": {
      "href": "/registration?action=register_login_identifier%40SESSION_ID",
      "inputs": {
        "email": {
          "name": "email",
          "type": "email",
          "max_length": 120,
          "required": true
        }
      },
      "action": "register_login_identifier",
      "description": "Enter an identifier to register."
    },
    "thirdparty_oauth": {
      "href": "/registration?action=thirdparty_oauth%40SESSION_ID",
      "inputs": {
        "provider": {
          "name": "provider",
          "type": "string",
          "required": true,
          "hidden": true,
          "allowed_values": [
            {
              "value": "discord",
              "name": "Discord"
            },
            {
              "value": "github",
              "name": "GitHub"
            },
            {
              "value": "google",
              "name": "Google"
            }
          ]
        },
        "redirect_to": {
          "name": "redirect_to",
          "type": "string",
          "required": true,
          "hidden": true
        }
      },
      "action": "thirdparty_oauth",
      "description": "Sign up/sign in with a third party provider via OAuth."
    }
  },
  "links": null
}

ここは Hanko Cloud の設定によって変わるが、今回は Email / OTP での登録を目指すので、今度は register_login_identifier の値を使用する。

const { csrf_token, actions } = registerClientCapabilitiesResponseJson;
const { register_login_identifier } = actions;
const { href } = register_login_identifier;
const registerLoginIdentifierResponse = await fetch(`https://${YOUR_HANKO_DOMAIN}/${href}`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    csrf_token,
    input_data: {
      email, // ユーザーから貰ったメールアドレス
    },
  }),
});
const registerLoginIdentifierResponseJson = await registerLoginIdentifierResponse.json();

同様に今度は次のようなレスポンスが返ってくる:

{
  "name": "passcode_confirmation",
  "status": 200,
  "payload": {},
  "csrf_token": "CSRF_TOKEN",
  "actions": {
    "back": {
      "href": "/registration?action=back%40SESSION_ID",
      "inputs": {},
      "action": "back",
      "description": "Navigate one step back."
    },
    "resend_passcode": {
      "href": "/registration?action=resend_passcode%40SESSION_ID",
      "inputs": {},
      "action": "resend_passcode",
      "description": "Send the passcode email again."
    },
    "verify_passcode": {
      "href": "/registration?action=verify_passcode%40SESSION_ID",
      "inputs": {
        "code": {
          "name": "code",
          "type": "string",
          "required": true
        }
      },
      "action": "verify_passcode",
      "description": "Enter a passcode."
    }
  },
  "links": null
}

この時点でユーザーにはメールアドレスの確認のメールが届いているはずなので、次は verify_passcode を使う。 ここでの code パラメーターには、ユーザーの元へ届いたメールのパスコードを入力して貰う。

const { csrf_token, actions } = registerLoginIdentifierResponseJson;
const { verify_passcode } = actions;
const { href } = verify_passcode;
const registrationResult = await fetch(`https://${YOUR_HANKO_DOMAIN}/${href}`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    csrf_token,
    input_data: {
      code, // ユーザーから貰ったパスコード
    },
  }),
});

これで、登録プロセスは完了。 registrationResult の結果が success であれば、 x-auth-token ヘッダーに有効な JWT が入っているので、これを Cookie に貼り付けるなり、煮るなり焼くなりすればアカウントが利用できる。

ということで、 Hanko での登録プロセスでした。お疲れさまでした。次回ログインプロセスに続く。かもしれない。