Skip to content

Keycloakを使ったパスキー(Passkey)の設定とTerraformでの自動化

はじめに

2023年あたりからパスキーの話をよく聞くようになり、Google へのログインもパスワードの入力からパスキーに代わり便利になりました。Keycloakは v23.0.0 からパスキーをサポートしており、パスワードレス認証を実装できます。

今回は、Keycloak のパスキーを使ったパスワードレス認証やパスキーの概要から実際の設定手順、さらにTerraformを使った自動化まで学習したので、そのときの学習内容をまとめました。

パスキーとは

パスキーは、パスワードに代わる新しい認証手段で、デバイス(スマホ、PC)に保存された公開鍵や生体認証情報を使って認証します。そのためユーザーがパスワードを入力することなく、認証を簡略化できます。“パスワード”と同じように、日常会話や体験の中で使う一般的な言葉として位置づけられています。

参考:

パスキーの利点

  • セキュリティの向上
  • ユーザーエクスペリエンスの向上
  • パスワードのリセットや管理の負担の軽減

単語

学習の際、気になった単語を列挙します。

FIDO アライアンス主に認証およびデバイス認証の標準の開発、使用、および準拠を促進する団体
WebAuthn Relying Party (RP)Web アプリケーションが Web Authentication API を利用してユーザを登録し、認証するエンティティ。
WebAuthnFIDOベースの認証を追加できるようにするウェブベースのAPI。FIDO2 の仕様の1部

成果物

https://github.com/kntks/blog-code/tree/main/2024/05/terraform-keycloak-authentication-flow

環境

バージョン
MacVentura 13.2.1
Keycloak24.0.3
Docker26.0.0
Docker Composev2.24.5
Terraform1.8.2

準備

Keycloakのセットアップ

Keycloak を docker compose で起動します。

compose.yaml
services:
keycloak:
image: quay.io/keycloak/keycloak:24.0.3
ports:
- target: 8080
published: 8080
protocol: tcp
mode: host
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
command: ["start-dev"]

Terraform を使用するために Keycloak側のセットアップ が必要です。

terraform/terraform.tfvars
client_id = "terraform"
client_secret = "client secretをコピペ"
url = "http://localhost:8080"

terraform apply で Keycloak の設定を行います。

Terminal window
cd terraform
terraform init
terraform plan
terraform apply -auto-approve

Keycloakのパスキー機能の概要と設定

Keycloak v23.0.0 から Authentication Flow を設定することで、パスキー機能を利用できます。 プレビュー機能として追加されたパスキーは、ユーザーがパスワードを入力する代わりに、メールや SMS などの方法で送られてくるパスコードを入力することで認証を行います。

W3C Web Authentication (WebAuthn) を読みながら、ログインフローを設定します。

デフォルトのログインフロー確認する

http://localhost:8080/realms/myrealm/account/ にアクセスするとユーザー名とパスワードを入れるログイン画面が表示されます。 先ほど Terraform を使ってユーザーアカウントを作成しました。ユーザー名とパスワードはともに myuser に設定しており、この認証情報を使えばアカウントコンソールにログインできます。 myrealm-account-console-login

デフォルトの設定では、ユーザー名とパスワードで認証するフローです。デフォルトのログインフローを確認します。

admin として Keycloak の管理画面にログインし、Authentication -> Flows から browser を選択します。 authentication-flows-list

これがデフォルトのログインフローです。Username Password Form が選択されていることがわかります。 authentication-flows-default-browser

ログインフローをパスキーに変更する

(デフォルトで On になっているはずですが)Authentication -> Required actions タブから Webauthn Register を有効にします。 authentication-required-actions-webauthn-register

Flow name が browser の右にある をクリックすると、Duplicate が表示されるので、選択します。 authentication-flows-duplicate-browser

フローの名前は、WebAuthn Browser にして、Duplicate をクリックします。 duplicate-flow-name

WebAuthn Browser Browser - Conditional OTP の右側にある、ゴミ箱アイコンをクリックして、このステップを削除します。
その後、WebAuthn Browser Forms の右側にある + アイコンをクリックして、Add step をクリックします。 authentication-flows-webauthn-browser-add-step-1

webauthn で検索して、WebAuthn Authenticator を選択します。 authentication-flows-webauthn-browser-add-webauthn-authenticator

WebAuthn AuthenticatorRequirementDisabled になっている場合は、Requiredにします。
以下が完成したログインフローは以下のようになります。 authentication-flows-webauthn-browser-complited

右上にある Action ボタンから Bind flow をクリックします。 authentication-flows-webauthn-browser-action-bind-flow

Browser flow のまま Save をクリックします。 authentication-flows-webauthn-browser-action-bind-flow-choose

パスキー設定後のログイン

これで設定は完了です。http://localhost:8080/realms/myrealm/account/ にアクセスするといつも通り、ユーザー名とパスワードを入れるログイン画面が表示されます。ユーザー名、パスワードともに myuser でログインしてみます。 myrealm-account-console-login

ログインに成功すると、パスキーの登録画面が表示されます。Register をクリックします。 myrealm-account-console-login-passkey

今、Chrome を使用しているので、自分の Chrome プロフィール を選択します。 myrealm-account-passkey-registration-1

続行 をクリックします。 myrealm-account-passkey-registration-2

パスワードを使用 をクリックします。 myrealm-account-passkey-registration-3

macOS のパスワードを入力する画面が表示されます。パスワードを入力して OK をクリックします。 myrealm-account-passkey-registration-4

これで次回からパスキーでログインできるようになりました。

Chrome のパスキーを確認する

Chrome を使っている場合、chrome://password-manager/settings にアクセスし Chrome プロフィールでパスキーを管理する をクリックするか、chrome://settings/passkeys に直接アクセスすることでパスキーを確認できます。 chrome-password-manager-settings chrome-settings-passkeys

ログインフォームをユーザー名のみに変更する

さきほどのフローでは、ユーザー名とパスワードを入力した後、パスキーの登録画面が表示されました。次はユーザー名とパスキーのみでログインできるように Authentication Flow を変更します。

ドキュメント通りにフローを設定する

Keycloak のドキュメントにある Passwordless WebAuthn together with Two-Factor を参考に前のセクションで設定した WebAuthn Browser フローを変更します。

passwordless-webauthn-together-with-two-factor

http://localhost:8080/realms/myrealm/account/ にアクセスするとユーザー名を入れるログイン画面が表示されます。

username-or-email

ユーザー名を入力後、パスワード入力画面が表示されますが、Try Another Way ボタンをクリックすると、パスキーでもログインできるようになります。 try-another-way password-or-passkey

パスキーを使ったログインフローをカスタマイズする

先ほどはドキュメント通りにフローを設定しましたが、パスキーを使うためには、ユーザー名を入力 -> Try Another Way ボタンをクリック -> パスキーを選択するという手順が必要です。この手順を省略して、ユーザー名を入力するとすぐにパスキーを使ってログインできるようにします。

フローは以下の画像のように設定します。このように設定することで、ユーザー名を入力するとすぐにパスキーを使ってログインできるようになります。
Password サブフローを作成することで、パスキーを設定していないユーザーも従来のパスワードでログインできます。 custom-webauthn-authentication-flow

ユーザー名を入力すると、以下のように画面遷移します

パスキー登録済みパスキー未登録
custom-webauthn-passkeycustom-webauthn-passkey-registration

Terraformを使用したPasskeyの自動化

このセクションでは、ひとつ前のセクションで手動で設定した認証フローを Terraform でコード化します。Terraform の import ブロックを使用して、先ほどカスタマイズした WebAuthn Passwordless フローをコード化します。

Admin API からリソース IDを取得する

import ブロックを使用して Authentication Flow のリソースを定義するためは、それぞれのリソースの ID を知る必要があります。

リソースimport フォーマット
keycloak_authentication_flow{{realmId}}/{{authenticationFlowId}}
keycloak_authentication_subflow{{realmId}}/{{parentFlowAlias}}/{{authenticationSubflowId}}
keycloak_authentication_execution{{realmId}}/{{parentFlowAlias}}/{{authenticationExecutionId}}

Authentication Flow の ID は、管理画面の URL が http://localhost:8080/admin/master/console/#/myrealm/authentication/{{ authenticationFlowId }}/notInUse のようになっているためわかります。しかし、authentication_subflow や authentication_execution の ID は管理画面に表示されていないため、Admin Rest API から取得する必要があります。

まずアクセストークンを発行します。準備の段階で、master releam に terrform クライアントを作成しているので、そのクライアントを使ってアクセストークンを取得します。

Terminal window
ACCESS_TOKEN=$(curl \
-d "client_id=terraform" \
-d "client_secret=" \
-d "grant_type=client_credentials" \
http://localhost:8080/realms/master/protocol/openid-connect/token | jq -r .access_token)

その後、/admin/realms/{realm}/authentication/flows/{flowAlias}/executions に GET リクエストを送ることで取得できます。

Terminal window
curl -H "Authorization: bearer $ACCESS_TOKEN" \
http://localhost:8080/admin/realms/myrealm/authentication/flows/WebAuthn%20Passwordless/executions | jq

idauthenticationExecutionId になり、flowIdauthenticationSubflowId になります。

レスポンス例
[
{
"id": "ed537e8e-f6ce-4d2d-88cd-be6577f6647d",
"requirement": "ALTERNATIVE",
"displayName": "Cookie",
"requirementChoices": [
"REQUIRED",
"ALTERNATIVE",
"DISABLED"
],
"configurable": false,
"providerId": "auth-cookie",
"level": 0,
"index": 0
},
{
"id": "b804adcd-ce1f-4f3f-ba69-1615057d58c4",
"requirement": "ALTERNATIVE",
"displayName": "Passwordless",
"description": "",
"requirementChoices": [
"REQUIRED",
"ALTERNATIVE",
"DISABLED",
"CONDITIONAL"
],
"configurable": false,
"authenticationFlow": true,
"flowId": "04c2525c-915b-457b-9b58-864595071645",
"level": 0,
"index": 1
},
...
]

import ブロックからリソースを生成する

以下のコードを import.tf に記述します。local ブロックを使用して、alias を定義します。alias は、フロー名である WebAuthn Passwordless です。import ブロックを使用して、keycloak_authentication_flowkeycloak_authentication_subflowkeycloak_authentication_execution リソースを定義します。

import.tf
# main.tf
# locals {
# realm_id = "myrealm"
# }
locals {
alias = "WebAuthn Passwordless"
}
import {
id = "${local.realm_id}/b24b39be-5287-4869-aac2-dcebb81df7fe"
to = keycloak_authentication_flow.passkey
}
import {
id = "${local.realm_id}/${local.alias}/ed537e8e-f6ce-4d2d-88cd-be6577f6647d"
to = keycloak_authentication_execution.cookie
}
import {
id = "${local.realm_id}/${local.alias}/04c2525c-915b-457b-9b58-864595071645"
to = keycloak_authentication_subflow.passwordless
}
import {
id = "${local.realm_id}/${local.alias}/ad32d3d8-e2a9-40cb-927d-e458f4f9c5e5"
to = keycloak_authentication_execution.username_form
}
import {
id = "${local.realm_id}/${local.alias}/0b464190-da0a-43a0-bb61-9f9f03bddfa9"
to = keycloak_authentication_execution.webauthn_authenticator
}
import {
id = "${local.realm_id}/${local.alias}/62c3f78f-95d0-45b7-9503-f98fde0943d3"
to = keycloak_authentication_subflow.password
}
import {
id = "${local.realm_id}/${local.alias}/27b810a9-07db-4fee-96f8-4acc49240633"
to = keycloak_authentication_execution.username_password_form
}

コードを生成します。

Terminal window
terraform plan -generate-config-out=generated.tf

以下は生成されたコードです。

generated.tf
generated.tf
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.
# __generated__ by Terraform from "myrealm/WebAuthn Passwordless/81c06618-4d3b-4903-9d91-e6264cb1fd85"
resource "keycloak_authentication_execution" "username_password_form" {
authenticator = "auth-username-password-form"
parent_flow_alias = "WebAuthn Passwordless"
realm_id = "myrealm"
requirement = "REQUIRED"
}
# __generated__ by Terraform from "myrealm/WebAuthn Passwordless/7f35dc08-71c5-47fa-96b9-3d7cb696c7bf"
resource "keycloak_authentication_execution" "webauthn_authenticator" {
authenticator = "webauthn-authenticator"
parent_flow_alias = "WebAuthn Passwordless"
realm_id = "myrealm"
requirement = "REQUIRED"
}
# __generated__ by Terraform from "myrealm/WebAuthn Passwordless/bc18a6ea-5ede-401d-bb4d-16dee8d7e5a7"
resource "keycloak_authentication_execution" "username_form" {
authenticator = "auth-username-form"
parent_flow_alias = "WebAuthn Passwordless"
realm_id = "myrealm"
requirement = "REQUIRED"
}
# __generated__ by Terraform from "myrealm/ab147574-8a30-4aa5-b402-b875cd7d9d94"
resource "keycloak_authentication_flow" "passkey" {
alias = "WebAuthn Passwordless"
description = null
provider_id = "basic-flow"
realm_id = "myrealm"
}
# __generated__ by Terraform from "myrealm/WebAuthn Passwordless/ea44fbd3-6146-41a3-a37d-10bed8ac5af9"
resource "keycloak_authentication_execution" "cookie" {
authenticator = "auth-cookie"
parent_flow_alias = "WebAuthn Passwordless"
realm_id = "myrealm"
requirement = "ALTERNATIVE"
}
# __generated__ by Terraform from "myrealm/WebAuthn Passwordless/33e814b0-a231-4ddd-8d08-a9923485f124"
resource "keycloak_authentication_subflow" "password" {
alias = "Password"
authenticator = null
description = null
parent_flow_alias = "WebAuthn Passwordless"
provider_id = "basic-flow"
realm_id = "myrealm"
requirement = "ALTERNATIVE"
}
# __generated__ by Terraform from "myrealm/WebAuthn Passwordless/825d4c31-ae32-49e8-83fe-05f668958240"
resource "keycloak_authentication_subflow" "passwordless" {
alias = "Passwordless"
authenticator = null
description = null
parent_flow_alias = "WebAuthn Passwordless"
provider_id = "basic-flow"
realm_id = "myrealm"
requirement = "ALTERNATIVE"
}

Authentication Flow のモジュール化

Terraform で plan, apply コマンドを実行するとき、ステップを含めたフローの作成や削除を繰り返すことがあります。そのため、フローをモジュール化して扱いやすくします。

ディレクトリ構造は以下のようになります。

Terminal window
$ tree modules
modules
└── webauthn-passwordless
├── main.tf
├── provider.tf
└── variables.tf

リファクタリングとモジュール化

terraform/generated.tf をリファクタリングして、webauthn-passwordless モジュールを作成します。

terraform/modules/webauthn-passwordless/provider.tf
terraform {
required_version = "~> 1.8.0"
required_providers {
keycloak = {
source = "mrparkers/keycloak"
version = ">= 4.4.0"
}
}
}
terraform/modules/webauthn-passwordless/variables.tf
variable "realm_id" {
type = string
description = "value of the realm id"
}
variable "flow_name" {
type = string
description = "value of the flow name"
}
variable "name_suffix" {
type = string
description = "value of the subflow name sufix"
default = ""
}

terraform/generated.tf -> terraform/modules/webauthn-passwordless/main.tf に移動します。

リファクタリングによる変更点は以下の通りです。

  • いくつかの keycloak_authentication_execution リソースの parent_flow_alias には、keycloak_authentication_subflow リソースの alias を指定する。
    • コード生成時は、keycloak_authentication_flow.passkey の alias 名だったため
  • keycloak_authentication_subflow リソースの alias は、keycloak_authentication_flow リソースの aliasname_suffix を組み合わせて生成する。
    • subflow の alias は、全Authentication Flow で一意でないといけないため(バグか仕様かわからない)
  • 各リソースの depends_on には、依存するリソースを指定する。
  • keycloak_authentication_bindings を作成し、レルムに認証フローをバインドする。
terraform/modules/webauthn-passwordless/main.tf
resource "keycloak_authentication_bindings" "browser_authentication_binding" {
realm_id = var.realm_id
browser_flow = keycloak_authentication_flow.passkey.alias
}
resource "keycloak_authentication_flow" "passkey" {
alias = var.flow_name
description = null
provider_id = "basic-flow"
realm_id = var.realm_id
}
resource "keycloak_authentication_execution" "cookie" {
authenticator = "auth-cookie"
parent_flow_alias = keycloak_authentication_flow.passkey.alias
realm_id = var.realm_id
requirement = "ALTERNATIVE"
depends_on = [keycloak_authentication_flow.passkey]
}
resource "keycloak_authentication_subflow" "passwordless" {
alias = join("-", compact(["Passwordless", var.name_suffix]))
authenticator = null
description = null
parent_flow_alias = keycloak_authentication_flow.passkey.alias
provider_id = "basic-flow"
realm_id = var.realm_id
requirement = "ALTERNATIVE"
depends_on = [
keycloak_authentication_execution.cookie,
keycloak_authentication_flow.passkey
]
}
resource "keycloak_authentication_execution" "username_form" {
authenticator = "auth-username-form"
parent_flow_alias = keycloak_authentication_subflow.passwordless.alias
realm_id = var.realm_id
requirement = "REQUIRED"
depends_on = [keycloak_authentication_subflow.passwordless]
}
resource "keycloak_authentication_execution" "webauthn_authenticator" {
authenticator = "webauthn-authenticator"
parent_flow_alias = keycloak_authentication_subflow.passwordless.alias
realm_id = var.realm_id
requirement = "REQUIRED"
depends_on = [
keycloak_authentication_subflow.passwordless,
keycloak_authentication_execution.username_form
]
}
resource "keycloak_authentication_subflow" "password" {
alias = join("-", compact(["Password", var.name_suffix]))
authenticator = null
description = null
parent_flow_alias = keycloak_authentication_flow.passkey.alias
provider_id = "basic-flow"
realm_id = var.realm_id
requirement = "ALTERNATIVE"
depends_on = [
keycloak_authentication_flow.passkey,
keycloak_authentication_subflow.passwordless
]
}
resource "keycloak_authentication_execution" "username_password_form" {
authenticator = "auth-username-password-form"
parent_flow_alias = keycloak_authentication_subflow.password.alias
realm_id = var.realm_id
requirement = "REQUIRED"
depends_on = [keycloak_authentication_subflow.password]
}

module を呼び出すために、terraform/main.tf に以下のコードを追加します。keycloak_realm リソースの browser_flow は削除します。

terraform/main.tf
resource "keycloak_realm" "myrealm" {
...
browser_flow = "browser"
...
}
module "webauthn_passwordless" {
source = "./modules/webauthn-passwordless"
realm_id = keycloak_realm.myrealm.id
flow_name = "WebAuthn Passwordless"
}

import ブロックを修正する

import.tf に記述した import ブロックを以下のように修正することで、モジュールのリソースとしてインポートできるようにします。

locals {
alias = "WebAuthn Passwordless"
}
import {
id = "${local.realm_id}/ab147574-8a30-4aa5-b402-b875cd7d9d94"
to = module.webauthn_passwordless.keycloak_authentication_flow.passkey
}
import {
id = "${local.realm_id}/${local.alias}/ea44fbd3-6146-41a3-a37d-10bed8ac5af9"
to = module.webauthn_passwordless.keycloak_authentication_execution.cookie
}
import {
id = "${local.realm_id}/${local.alias}/825d4c31-ae32-49e8-83fe-05f668958240"
to = module.webauthn_passwordless.keycloak_authentication_subflow.passwordless
}
import {
id = "${local.realm_id}/${local.alias}/bc18a6ea-5ede-401d-bb4d-16dee8d7e5a7"
to = module.webauthn_passwordless.keycloak_authentication_execution.username_form
}
import {
id = "${local.realm_id}/${local.alias}/7f35dc08-71c5-47fa-96b9-3d7cb696c7bf"
to = module.webauthn_passwordless.keycloak_authentication_execution.webauthn_authenticator
}
import {
id = "${local.realm_id}/${local.alias}/33e814b0-a231-4ddd-8d08-a9923485f124"
to = module.webauthn_passwordless.keycloak_authentication_subflow.password
}
import {
id = "${local.realm_id}/${local.alias}/81c06618-4d3b-4903-9d91-e6264cb1fd85"
to = module.webauthn_passwordless.keycloak_authentication_execution.username_password_form
}

インポートします。

Terminal window
terraform apply -auto-approve

モジュール化した Authentication Flow の完成です。

さいごに

WebAuthn や FIDO に関する仕様は、まだまだ理解が追いついていません。しかし、Keycloak で簡単に設定できるので、実際に設定してみることで理解が深まりました。また、Terraform を使ってコード化することで、設定を繰り返す際にも便利です。

関連リンク