#OpenAM で FIDO2(WebAuthn) 認証を実装する
今年はブラウザの対応が進んだことから、だいぶ盛り上がったFIDO2認証です。
OpenAM FIDO2(WebAuthn)ユースケースを考え たので、
認証モジュールを書いて、パスワード「よりよい世界に」しましょうかねww
#OpenAMの認証モジュール
OpenAMの認証機能はモジュール単位になっており、OpenAMコンソーシアムのツリーでは以下のディレクトリにあります。
https://github.com/openam-jp/openam/tree/master/openam-authentication
ディレクトリ毎に、それぞれ独立した認証機能を提供しています。
##OpenAM認証モジュールの概要
OpenAMはフロントエンドにBackbone.jsを使っており、
ブラウザとの通信は json のやり取りが主です。
jsonの中に Callback が含まれていて、それを元にバックエンドが認証処理を進めていきます。
認証モジュールにはステートという状態を管理するための機能があり、
Callbackの内容や、処理の状況に合わせてステートを変え、処理内容を遷移させて行きます。
サンプルの認証モジュール(SampleAuth.java)のコードを抜粋すると
/**
* SampleAuth authentication module example.
* AMLoginModuleを元にしています。
*/
public class SampleAuth extends AMLoginModule {
...
//認証モジュールの初期化処理、設定値の取得など。
public void init(Subject subject, Map sharedState, Map options) {
debug.message("SampleAuth::init");
this.options = options;
...
}
//認証モジュールの処理本体 引数はブラウザからの応答がcallbacksでstateは現時点での処理すべきstateです。
public int process(Callback[] callbacks, int state) throws LoginException {
debug.message("SampleAuth::process state: {}", state);
//認証モジュールのstate毎に処理内容が移替わっていきます。
switch (state) {
...
//STATE_AUTH 認証画面から送信されたCallbackを受け取って処理する
case STATE_AUTH:
// Get data from callbacks. Refer to callbacks XML file.
//ブラウザに入力されたユーザーIDの取得
NameCallback nc = (NameCallback) callbacks[0]
//ブラウザに入力されたパスワードの取得
PasswordCallback pc = (PasswordCallback) callbacks[1];
String username = nc.getName();
String password = String.valueOf(pc.getPassword());
//...続く
粛々wwと処理が進んで最終的には LOGIN_SUCCEED となってこのモジュール自体は終了します。
//usernameとpasswordの確認をして成功を返す。
if (USERNAME.equals(username) && PASSWORD.equals(password)) {
debug.message("SampleAuth::process User '{}' " +
"authenticated with success.", username);
return ISAuthConstants.LOGIN_SUCCEED;
}
#FIDO2(WebAuthn)認証モジュールの実装内容
認証モジュールの概要がわかったところで、FIDO2のシーケンスを実装に落とし込んでみます。
シーケンスとその間のデータについてはFIDOアライアンスのチュートリアルが参考になります。
もちろんW3Cの方も...
ぜひ日本語でという方はYahoo! JAPANでの生体認証の取り組み(FIDO2サーバーの仕組みについて)が良いと思います。
Window Helloであれば、Web Authentication and Windows Helloをご参照ください。
##登録シーケンス
OpenAM FIDO2(WebAuthn)ユースケース 登録の図の再掲です。
##認証シーケンス
OpenAM FIDO2(WebAuthn)ユースケース 認証の図の再掲です。
##バックエンドの処理 その1
登録、認証どちらのシーケンスもログインIDを受け取るところを起点としています。
ここは、普通にCallbackを受け取れば良いので問題ありません。
その後、登録シーケンスの図には無いですが、認証シーケンスと同じく**[Authenticatorに渡したいオプション]**と challenge をブラウザへ送り、ブラウザからの戻ってきた challenge を検証するので、challenge を一旦記憶する必要があります。
これはFIDO2Serviceというインスタンスを、認証モジュールの初期化処理で作成するようにします。
これで、認証モジュールの生存期間(ブラウザとの一時的なセッション)中で参照できます。
FIDO2Serviceインスタンス
public FIDO2Service(String challenge) {
this.challenge = challenge;
FIDO2.java
/**
* FIDO2 authentication module.
*/
public class FIDO2 extends AMLoginModule {
...
//認証モジュールの初期化処理、設定値の取得など。
public void init(Subject subject, Map sharedState, Map options) {
debug.message("FIDO2::init");
this.options = options;
...
fido2Service = new FIDO2Service(challenge);
...
//認証モジュールの処理本体
public int process(Callback[] callbacks, int state) throws LoginException {
...
chalenge 以外の情報 [Authenticatorに渡したいオプション] は、 rp、user、pubKeyCredParams などですが、これは適当に生成し、まとめてTextOutputCallbackでブラウザへ送ります。
##フロントエンドの処理
FIDO2の場合、入力フォームにパスワードを入れる替わりにAuthenticatorの処理結果をPOSTします。
普通のForm認証ログイン画面では馴染みのない処理となります。
###登録の処理
画面としては、このようなダイアログ(Firefoxの場合)が出ます。
この処理は、Javascriptの **navigator.credential.create(RPサーバーが送ったオプション)**を実行することにより出ています。
###認証の処理
画面としては、このようなダイアログ(Firefoxの場合)が出ます。
この処理は、Javascriptの **navigator.credential.get(RPサーバーが送ったオプション)**を実行することにより出ています。
どちらも処理結果をサーバーへPOSTする必要がありますので、以下のような感じになります。
登録時の navigator.credentials.create 例
/*
* サーバーから来たmakeCredentialOptions
* を引数にnavigator.credentials.createを実行
*/
navigator.credentials.create({
publicKey: makeCredentialOptions
}).then(function (newCredential) {
/*
*Authenticatorの処理結果をArrayBufferに
* attestationObject
* clientDataJSON
* rawID
*/
let attestationObject = new Uint8Array(newCredential.response.attestationObject);
let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
let rawId = new Uint8Array(newCredential.rawId);
//RPサーバーへポスト
$.post('/openam/json/authenticate', {
id: newCredential.id,
rawId: b64enc(rawId),
type: newCredential.type,
attObj: b64RawEnc(attestationObject),
clientData: b64RawEnc(clientDataJSON),
})
});
##バックエンドの処理 その2
ブラウザからPOSTされたデーターを処理していきます。
###登録
登録の場合はデーターにattestationObjectが含まれます。
実行すべき処理は公開鍵情報を取り出してユーザーデーターストアに格納することです。
公開鍵情報を取り出す。と簡単に書きましたがCBORー>COSEー>PKCSと処理します。
取り出した公開鍵を無事登録できたら、モジュールのステートは振り出しに戻り、ログイン画面へ。
黄色がブラウザ側フロントエンド 青がサーバー側バックエンド(OpenAM)です。
###認証
認証の場合はデーターにAuthenticatorDataが含まれます。
実行すべき処理はユーザーデーターストアにある公開鍵でchallengeの署名を検証することです。
OpenAMとしては冒頭の LOGIN_SUCCEED までくれば認証モジュールは役割を終えます。
黄色がブラウザ側フロントエンド 青がサーバー側バックエンド(OpenAM)です。
###実食
コードが書けたら早速試して見るのです。
Windows Helloで試してみました。
##設定画面
おまけ
認証モジュールの設定画面で設定する項目はFIDO2のオプションで、サーバーとして認証時に指定したいものを選びます。
#これから
セルフテスト用に各種Authenticatorも買って行きたい
よさげな場所(CA or Europ)で開催されるインターオペラビリティテストに行きたい...