1. 詳細
1.1. クレデンシャル情報の表現
クレデンシャル情報を表現するインタフェースとして、 CredentialRecord インタフェースが存在します。 登録時に、 RegistrationData クラスが含む値を用いて CredentialRecord インタフェースの インスタンスを作成し、アプリケーションの作法に則り永続化してください。認証時の検証に必要となります。 なお、永続化する際は、検索する際の利便性を考え、credentialIdをキーに永続化すると良いでしょう。
CredentialRecord インタフェースの実装クラスを設計する際は、アプリケーションの要件に合わせて拡張すると良いでしょう。 典型的には、 CredentialRecord をユーザーが識別するための名前フィールドの追加などが考えられるでしょう。
1.2. CredentialRecordのシリアライズ、デシリアライズ
認証デバイスの登録時、CredentialRecord のインスタンスをデータベース等に永続化し、認証時に利用できるようにするのはアプリケーションの責務ですが、
CredentialRecord を構成する各メンバをシリアライズ、デシリアライズする際に使用できるクラスをWebAuthn4Jでは用意しています。 アプリケーションで永続化を実装する際の補助としてご利用ください。
1.2.1. attestedCredentialData
AttestedCredentialDataConverter で AttestedCredentialData を byte[] に変換したり、その逆の変換を行うことが出来ます。 なお、String として保存したい場合、さらに Base64UrlUtil を用いることで byte[] から Base64Url String に変換することが出来ます。
AttestedCredentialDataConverter attestedCredentialDataConverter = new AttestedCredentialDataConverter(objectConverter);
// serialize
byte[] serialized = attestedCredentialDataConverter.convert(attestedCredentialData);
// deserialize
AttestedCredentialData deserialized = attestedCredentialDataConverter.convert(serialized);
1.2.2. attestationStatement
AttestationStatement はインタフェースであり、PackedAttestationStatement や、AndroidKeyAttestationStatement など、フォーマットにあわせて複数の実装があります。
AttestationStatement のCBOR表現は具象型が何であるかについて自己記述性を持たない為、フォーマットは別途、別のフィールドとして永続化する必要があります。 そのため、CBORとしてシリアライズする際は、実際のattestationStatementのフィールドと、フォーマットのフィールドを持つエンベロープクラスを用意し、エンベロープクラスをシリアライズしなければなりません。
エンベロープクラス自体は、WebAuthn4Jライブラリでは提供していないため、以下の例を参考にアプリケーションコード側で実装下さい。
//serialize
AttestationStatementEnvelope envelope = new AttestationStatementEnvelope(attestationStatement);
byte[] serializedEnvelope = objectConverter.getCborMapper().writeValueAsBytes(envelope);
//deserialize
AttestationStatementEnvelope deserializedEnvelope = objectConverter.getCborMapper().readValue(serializedEnvelope, AttestationStatementEnvelope.class);
AttestationStatement deserializedAttestationStatement = deserializedEnvelope.getAttestationStatement();
class AttestationStatementEnvelope{
@JsonProperty("attStmt")
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "fmt"
)
private AttestationStatement attestationStatement;
@JsonCreator
public AttestationStatementEnvelope(@JsonProperty("attStmt") AttestationStatement attestationStatement) {
this.attestationStatement = attestationStatement;
}
@JsonProperty("fmt")
public String getFormat() {
return attestationStatement.getFormat();
}
public AttestationStatement getAttestationStatement() {
return attestationStatement;
}
}
1.2.3. transports
JSON文字列として保存したい場合は ObjectConverter が使用できます。
String serializedTransports = objectConverter.getJsonMapper().writeValueAsString(transports);
1.3. DCAppleDeviceのシリアライズ、デシリアライズ
webauthn4j-appattestでは、CredentialRecord インタフェースの代わりに、DCAppleDevice インタフェースを実装したクラスを、構成証明の検証時とアサーションの検証時の間で永続化する必要があります。 概ねCredentialRecordのシリアライズ、デシリアライズで解説した方法でシリアライズ、デシリアライズが可能ですが、一点気を付ける必要がある点として、 webauthn4j-appattest独自のクラス(例えば AppleAppAttestAttestationStatement )のシリアライズ、デシリアライズを行う為に、
ObjectConverter は DeviceCheckCBORModule が登録されたものを使用する必要があります。
DeviceCheckCBORModule が登録された ObjectConverter は DeviceCheckManager#createObjectConverter で得ることが出来ます。
1.4. Safariで未サポートなJSON serialization APIsの代替
クイックスタートでは、 PublicKeyCredentialCreationOptions をパースするAPIとして PublicKeyCredential.parseCreationOptionsFromJSON が、
PublicKeyCredential をシリアライズするAPIとして PublicKeyCredential#toJSON が存在すると紹介しましたが、Safariでは18.4以降でしか利用できません。
代わりとして、GitHubが提供するnpmライブラリ、 github/@webauthn-json が提供する、 pony-fillを利用するのがお勧めです。
PublicKeyCredential.parseCreationOptionsFromJSON の代わりに、 parseCreationOptionsFromJSON が、
naviagator.credentials.create の代わりに create が提供されています。
import {
create,
parseCreationOptionsFromJSON,
} from "@github/webauthn-json/browser-ponyfill";
const response = await fetch("<endpoint path that returns PublicKeyCredentialCreationOptions as JSON>") //fetch PublicKeyCredentialCreationOptions as JSON string
const publicKeyCredentialCreationOptionsJSON = await response.json() // convert to JSONObject
const credentialCreationOptions = parseCreationOptionsFromJSON(publicKeyCredentialCreationOptionsJSON); // convert to PublicKeyCredentialCreationOptions
const publicKeyCredential = await create({ publicKey: credentialCreationOptions}); // create PublicKeyCredential
const registrationResponseJSON = publicKeyCredential.toJSON() // JSON object of publicKeyCredential
const registrationResponseJSONStr = JSON.stringify(registrationResponseJSON) // JSON string representation of publicKeyCredential
このpony-fillの create メソッドを利用して得られた publicKeyCredentialでは、 toJSON メソッドが利用可能です。
PublicKeyCredential.parseRequestOptionsFromJSON の代わりとしては、 parseRequestOptionsFromJSON が、
naviagator.credentials.get の代わりに get が提供されています。
import {
get,
parseRequestOptionsFromJSON,
} from "@github/webauthn-json/browser-ponyfill";
const response = await fetch("<endpoint path that returns PublicKeyCredentialRequestOptions as JSON>");
const publicKeyCredentialRequestOptionsJSON = await response.json();
const credentialGetOptions = parseRequestOptionsFromJSON(publicKeyCredentialRequestOptionsJSON);
const publicKeyCredential = await get({ publicKey: credentialGetOptions});
const authenticationResponseJSON = publicKeyCredential.toJSON()
const authenticationResponseJSONStr = JSON.stringify(authenticationResponseJSON)
1.5. モジュール構成
WebAuthn4Jは、以下のModuleから構成されます。
1.5.4. Metadata-Async: webauthn4j-metadata-async.jar
FIDO Metadata Serviceを用いたTrustAnchorの解決など、追加的な機能の非同期バージョンを提供します。
1.6. Origin検証
WebAuthn の「Origin 検証」とは、登録・認証の各セレモニー時点でクライアント(ブラウザ)が認識していた Origin が、RP が期待する Origin と一致するかをサーバー側で検証することを指します。 これは、クライアントが生成する clientData に内包される origin を根拠に、セレモニーが想定したページから正しく開始されたことを確認し、他オリジンからの不正な呼び出しを防止する目的があります。
また、iframe 配下でのクロスオリジンな操作(例: https://example.com のページに埋め込まれた https://rp.example.net の iframe 内でWebAuthn認証を実行するような操作)について、WebAuthn は原則として許容しません。 これは、同一オリジン原則に基づき、クリックジャッキング等のフレーミング攻撃や別オリジンからの恣意的な認証誘発を抑止するためです。
一方で、認証ウィジェットを別オリジンのサイトに iframe 埋め込みする構成(自社・パートナーサイトなど)のようなユースケースでは、適切に制約したうえでクロスオリジンの実行を許容したい場合があります。 Permissions-Policyディレクティブなどを使用して許可を宣言することで、クロスオリジンでのWebAuthnの使用をクライアントに対して許可することが可能ですが、 サーバーサイドでも適切に検証が必要であり、WebAuthn4J では、ServerProperty のビルダー API により、通常の origin(RP ページ)に対する検証に加えて、topOrigin(親ページ)の許容条件を宣言的に構成できます。 既定ではクロスオリジンは許可されません。クロスオリジンを許容する必要がある場合は、許容したい親ページを明示する topOrigin(…)/topOrigins(…)/topOriginPredicate(…) もしくは任意許可の anyTopOrigin() を設定してください。
1.6.1. origin の検証
-
指定方法
-
origin(…), origins(…), originPredicate(…): RP ページの許容オリジンを指定します。
-
-
既定の挙動
-
指定した条件に一致するオリジンのみを許容します。
-
-
使用例
ServerProperty serverProperty = ServerProperty.builder()
.origin(new Origin("https://rp.example.com"))
.rpId("rp.example.com")
.challenge(challenge)
.build();
1.6.2. topOrigin の検証
-
目的
-
iframe 配下でのクロスオリジンな WebAuthn 操作において、最上位ドキュメント(親ページ)のオリジンを検証し、想定する埋め込み元のみを許容します。
-
-
指定方法
-
topOrigin(…), topOrigins(…), topOriginPredicate(…): 許可するトップオリジンを限定
-
anyTopOrigin(): すべてのトップオリジンを許可(利便性は高いがセキュリティ境界を広げるため慎重に使用)
-
-
既定の挙動
-
明示的に topOrigin を設定しない場合、クロスオリジンな iframe からの利用は許可されません。
-
クライアントから topOrigin が提供されない場合は、通常の origin 検証のみが行われます。
-
-
使用例(特定のトップオリジンのみ許可)
ServerProperty serverProperty = ServerProperty.builder()
.origin(new Origin("https://rp.example.com"))
.rpId("rp.example.com")
.challenge(challenge)
.topOrigin(new Origin("https://parent.example.com"))
.build();
-
使用例(任意のトップオリジンを許可)
ServerProperty serverProperty = ServerProperty.builder()
.origin(new Origin("https://rp.example.com"))
.rpId("rp.example.com")
.challenge(challenge)
.anyTopOrigin()
.build();
-
セキュリティ考慮
-
任意許可(anyTopOrigin)は攻撃面の拡大につながる可能性があります。可能な限り topOrigin/topOrigins/topOriginPredicate により許可範囲を限定してください。
-
-
後方互換
-
旧来の包括的許可フラグは後方互換のために残されていますが、新規の構成では topOriginPredicate(または anyTopOrigin)を使用してください。
-
1.7. カスタムな検証ロジックの実装
WebAuthn4Jでは、カスタムな検証ロジックを実装し、追加することが可能です。 登録時の検証にカスタムロジックを追加する場合は、 CustomRegistrationVerifier を実装してください。 認証時の検証にカスタムロジックを追加する場合は、 CustomAuthenticationVerifier を実装してください。
1.8. カスタムなデータ変換ロジックの実装
WebAuthn4Jでは、JSONやCBORのシリアライズ、デシリアライズ処理にJackson 3ライブラリを使用しています。
カスタムなシリアライザ、デシリアライザを使用するには、それらを登録したJackson 3の JsonMapper 、 CBORMapper をWebAuthn4Jに渡す必要があります。
1.8.1. カスタムなデータ変換ロジックの登録
WebAuthn4Jは、Jackson 3の JsonMapper, CBORMapper のセットを、 ObjectConverter というクラスのインスタンスを通じて受け取ります。
カスタムなシリアライザ、デシリアライザを登録した JsonMapper, CBORMapper を ObjectConverter インスタンス作成時にコンストラクタからインジェクトし、
その ObjectConverter を WebAuthnManager のインスタンス作成時にパラメータとして指定してください。
1.8.2. 外部モジュールからのWebAuthn拡張の追加
WebAuthn4Jでは、webauthn4j-core外部のモジュールから独自のWebAuthn拡張を定義し、coreの変更なしに追加できます。
AuthenticationExtensionsClientOutputs 等の拡張コンテナは、内部的に生データを ObjectNode として保持しており、
各拡張のデシリアライズはJackson Moduleに登録されたデシリアライザを通じて遅延的に行われます。
外部モジュールは、自前のJackson Moduleに拡張のデシリアライザを登録し、そのModuleを含む ObjectConverter をWebAuthn4Jに渡すことで、独自の拡張を追加できます。
Client Extension Output(JSON)の追加例
以下は、架空の拡張 exampleExtension を外部モジュールから追加する例です。
この拡張は以下のように定義されているものとします。
partial dictionary AuthenticationExtensionsClientOutputs {
ExampleExtensionOutput exampleExtension;
};
dictionary ExampleExtensionOutput {
required USVString exampleValue;
boolean exampleFlag;
};
この定義に基づくと、Client Extension Outputは以下のようなJSONとして返されます。
{
"exampleExtension": {
"exampleValue": "hello",
"exampleFlag": true
}
}
以下に、この拡張をWebAuthn4Jで扱えるようにする手順を示します。
1. 拡張のデータクラスを定義する
public class ExampleExtensionClientOutput
implements AuthenticationExtensionClientOutput {
private final String exampleValue;
private final Boolean exampleFlag;
@JsonCreator
public ExampleExtensionClientOutput(
@JsonProperty("exampleValue") String exampleValue,
@JsonProperty("exampleFlag") Boolean exampleFlag) {
this.exampleValue = exampleValue;
this.exampleFlag = exampleFlag;
}
@Override
public @NotNull String getIdentifier() {
return "exampleExtension";
}
public @NotNull String getExampleValue() {
return exampleValue;
}
public @Nullable Boolean getExampleFlag() {
return exampleFlag;
}
@Override
public void validate() {
if (exampleValue == null) {
throw new ConstraintViolationException("exampleValue must not be null");
}
}
}
2. ExtensionClientOutputDeserializer を継承したデシリアライザを実装する
デシリアライザは AuthenticationExtensionsClientOutputs 全体の ObjectNode を受け取り、
自分が担当するキー("exampleExtension")の値を取り出して変換します。
キーが存在しない場合は null を返します。
public class ExampleExtensionClientOutputDeserializer
extends ExtensionClientOutputDeserializer<ExampleExtensionClientOutput> {
public ExampleExtensionClientOutputDeserializer() {
super(ExampleExtensionClientOutput.class);
}
@Override
public @NotNull Class<ExampleExtensionClientOutput> getType() {
return ExampleExtensionClientOutput.class;
}
@Override
public @NotNull Set<String> getKeys() {
return Set.of("exampleExtension");
}
@Override
public ExampleExtensionClientOutput deserialize(
JsonParser p, DeserializationContext ctxt) {
ObjectNode node = (ObjectNode) p.readValueAsTree();
JsonNode value = node.get("exampleExtension");
if (value == null || value.isNull()) return null;
// Construct directly from JSON fields rather than using readTreeAsValue
// with the same class, which would recursively invoke this deserializer.
String exampleValue = value.has("exampleValue") ? value.get("exampleValue").asText() : null;
Boolean exampleFlag = value.has("exampleFlag") ? value.get("exampleFlag").asBoolean() : null;
return new ExampleExtensionClientOutput(exampleValue, exampleFlag);
}
}
3. Jackson Moduleを作成し、デシリアライザを登録する
public class ExampleExtensionModule extends SimpleModule {
public ExampleExtensionModule() {
super("ExampleExtensionModule");
this.addDeserializer(ExampleExtensionClientOutput.class,
new ExampleExtensionClientOutputDeserializer());
}
}
4. このModuleを含む ObjectConverter を作成し、 WebAuthnManager に渡す
ObjectConverter は渡された JsonMapper / CBORMapper に WebAuthnJSONModule / WebAuthnCBORModule を
自動的に追加するため、外部モジュールのみ登録すればよい。
JsonMapper jsonMapper = JsonMapper.builder()
.addModule(new ExampleExtensionModule())
.build();
ObjectConverter objectConverter = new ObjectConverter(jsonMapper, new CBORMapper());
5. getExtension(Class) で型安全に取得する
ExampleExtensionClientOutput example =
clientOutputs.getExtension(ExampleExtensionClientOutput.class);
String value = example.getExampleValue();
その他の拡張ポイント
同様のパターンで、以下の拡張も追加できます。
| 種別 | 基底デシリアライザクラス | 登録先 |
|---|---|---|
Client Extension Input |
|
|
Authenticator Extension Output |
|
|
Authenticator Extension Input |
|
|
1.9. クラス
1.9.2. Converter, WebAuthnModule
データパッケージ配下のクラスはJacksonによってシリアライズ、デシリアライズ可能なように設計されています。 一部のクラスはカスタムなシリアライザ、デシリアライザが必要であり、 converter パッケージ配下に集約されています。 カスタムシリアライザ、デシリアライザは WebAuthnJSONModule と WebAuthnCBORModule というJacksonのModuleにまとめられています。 ObjectConverter は内部で使用するJacksonの JsonMapper, CBORMapper に自動で WebAuthnJSONModule と WebAuthnCBORModule を適用しますが、ObjectConverter の外部で WebAuthn4Jのシリアライザ、デシリアライザを使用したい場合は、Jacksonの JsonMapper, CBORMapper に WebAuthnJSONModule と WebAuthnCBORModule を登録すると良いでしょう。
FIDO Metadata Service (MDS) の型(AAID、AuthenticatorStatus 等)を扱う場合は、WebAuthnMetadataJSONModule が対応するシリアライザ、デシリアライザを提供します。
現在、MetadataBLOBFactory、LocalFilesMetadataStatementsProvider、LocalFilesMetadataStatementsAsyncProvider 等のメタデータエントリーポイントクラスは、WebAuthnMetadataJSONModule が未登録の場合にフォールバックのシリアライザ、デシリアライザを自動登録します(警告ログが出力されます)。
将来のリリースでは WebAuthnMetadataJSONModule の明示的な登録が必須になる予定です。
モジュールの登録には ObjectConverter.rebuildWithJSONModule を使用してください:
ObjectConverter objectConverter = new ObjectConverter()
.rebuildWithJSONModule(new WebAuthnMetadataJSONModule());
1.9.3. TrustAnchorsResolver
TrustAnchorsResolver インタフェースは TrustAnchorCertPathTrustworthinessVerifier で構成証明ステートメントの信頼性の 検証を行う際に信頼するルート証明書のセットを探索するために使用されます。
1.9.4. TrustAnchorsProvider
TrustAnchorsProvider インタフェースは前述の TrustAnchorsResolver インタフェースの実装である TrustAnchorsResolverImpl
がTrustAnchorの読込処理を委譲する先のインタフェースです。実装としてJava Key StoreファイルからTrustAnchorを読み込む
KeyStoreFileTrustAnchorsProvider クラスが提供されている他、WebAuthn4J Spring Securityでは、SpringのResourceから TrustAnchorを読み込む CertFileResourcesTrustAnchorProvider が提供されています。
1.10. WebAuthn以外のFIDO CTAP2セキュリティキーを用いた独自アプリケーションでの利用
FIDO CTAP2セキュリティキーにとって、WebAuthnは一つの応用例でしかなく、セキュアな認証を必要とする独自アプリケーションで セキュリティキーを利用することも可能です。本節では、FIDO CTAP2セキュリティキーを用いた独自アプリケーションにおけるAttestation、Assertion検証でWebAuthn4Jを利用する方法を説明します。
1.10.1. FIDO CTAP2セキュリティキーを用いた独自アプリケーションでの登録、認証のフロー
FIDO CTAP2セキュリティキーを独自アプリケーションで認証に使用する場合、セキュリティキーを登録するために、 アプリからFIDO CTAP2セキュリティキーの authenticatorMakeCredential メソッドを呼び出し、公開鍵やデバイスの構成情報を 含むデータ(構成証明、Attestation)を取得し保存します。 取得されたAttestationは、セキュリティキーがアプリとして受け入れ可能なキーか判定するために検証が必要です。 WebAuthn4Jでは、 CoreRegistrationVerifier クラスを用いることで、取得されたAttestationを検証可能です。
認証時には、同様にアプリからFIDO CTAP2セキュリティキーの authenticatorGetAssertion メソッドを呼び出し、認証時にサーバーに送信される署名を含んだデータ(アサーション、Assertion)を取得します。 取得されたAssertionを検証することで、アプリは認証に用いられたセキュリティキーが、登録時に用いられたセキュリティキーと同一であることを確認し、正当なアクセスか判定することが可能となります。WebAuthn4Jでは、 CoreAuthenticationVerifier クラスを用いることで、取得されたAssertionを検証可能です。
1.10.2. アプリケーション固有のクライアントデータの真正性の担保、検証
上記のフローに従って実装することで、FIDO CTAP2セキュリティキーを用いた安全な認証が実現可能ですが、 FIDO CTAP2セキュリティキーを呼び出す主体(クライアント)と、Attestation、Assertionを検証する主体(サーバー)が分離している場合、クライアントが登録、認証時にアプリケーション固有のクライアントデータを生成し、クライアントデータを追加でサーバーで検証したい場合もあります。クライアントデータ自体はAttestation、Assertionと一緒に送信すれば良いですが、 クライアントデータを中間者攻撃から防御するために、クライアントデータに対して署名を行い、保護する必要があります。
さて、FIDO CTAP2では、登録時に利用する authenticatorMakeCredential メソッドと認証時に利用する authenticatorGetAssertion メソッド 、どちらにも共通するパラメータとして、clientDataHash というパラメータが存在します。セキュリティキーは、受け取った clientDataHash パラメータを署名対象のデータの一部として署名を生成するため、アプリケーションとして署名で保護したいクライアントデータのハッシュを取得し、
clientDataHash にセットすることで、アプリケーション固有のクライアントデータが改竄されていない真正なデータか、サーバー側で検証することが出来ます。