OpenAM で FIDO2(WebAuthn) 認証を実装する

#OpenAM で FIDO2(WebAuthn) 認証を実装する
今年はブラウザの対応が進んだことから、だいぶ盛り上がったFIDO2認証です。
[OpenAM FIDO2(WebAuthn)ユースケースを考え](https://qiita.com/tonoki/items/16a99d770fa496bdaafc)　たので、
認証モジュールを書いて、パスワード「よりよい世界に」しましょうかねww

#OpenAMの認証モジュール
OpenAMの認証機能はモジュール単位になっており、OpenAMコンソーシアムのツリーでは以下のディレクトリにあります。
[https://github.com/openam-jp/openam/tree/master/openam-authentication](https://github.com/openam-jp/openam/tree/master/openam-authentication)
ディレクトリ毎に、それぞれ独立した認証機能を提供しています。

##OpenAM認証モジュールの概要
OpenAMはフロントエンドにBackbone.jsを使っており、
ブラウザとの通信は **json** のやり取りが主です。
jsonの中に **Callback** が含まれていて、それを元にバックエンドが認証処理を進めていきます。
認証モジュールにはステートという状態を管理するための機能があり、
Callbackの内容や、処理の状況に合わせてステートを変え、処理内容を遷移させて行きます。
サンプルの認証モジュール(SampleAuth.java)のコードを抜粋すると

```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** となってこのモジュール自体は終了します。

```java
                //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アライアンスのチュートリアル](https://slides.com/fidoalliance/jan-2018-fido-seminar-webauthn-tutorial#/)が参考になります。
もちろん[W3C](https://www.w3.org/TR/webauthn/)の方も...
ぜひ日本語でという方は[Yahoo! JAPANでの生体認証の取り組み（FIDO2サーバーの仕組みについて）](https://techblog.yahoo.co.jp/advent-calendar-2018/webauthn/)が良いと思います。
Window Helloであれば、[Web Authentication and Windows Hello](https://docs.microsoft.com/en-us/microsoft-edge/dev-guide/windows-integration/web-authentication)をご参照ください。

##登録シーケンス
[OpenAM FIDO2(WebAuthn)ユースケース　登録](https://qiita.com/tonoki/items/16a99d770fa496bdaafc#%E7%99%BB%E9%8C%B2)の図の再掲です。
![fido2reg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316098/ac203362-d407-7e81-6cb2-de1783804b56.png)


##認証シーケンス
[OpenAM FIDO2(WebAuthn)ユースケース　認証](https://qiita.com/tonoki/items/16a99d770fa496bdaafc)の図の再掲です。
![fido2auth.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316098/95fd55be-f40e-8ef9-1691-fb6955d100fe.png)


##バックエンドの処理 その1
登録、認証どちらのシーケンスもログインIDを受け取るところを起点としています。
ここは、普通にCallbackを受け取れば良いので問題ありません。
その後、登録シーケンスの図には無いですが、認証シーケンスと同じく**[Authenticatorに渡したいオプション]**と **challenge** をブラウザへ送り、ブラウザからの戻ってきた **challenge** を検証するので、**challenge** を一旦記憶する必要があります。

これはFIDO2Serviceというインスタンスを、認証モジュールの初期化処理で作成するようにします。
これで、認証モジュールの生存期間（ブラウザとの一時的なセッション）中で参照できます。

FIDO2Serviceインスタンス

```java
public FIDO2Service(String challenge) {
        this.challenge = challenge;
```

FIDO2.java

```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サーバーが送ったオプション)**を実行することにより出ています。
![fido-5.png](https://qiita-image-store.s3.amazonaws.com/0/316098/409a8480-9efb-770c-9aec-b2a3b4ada2ea.png)

RPサーバーが送ったcreateオプションの例
![createoptions.png](https://qiita-image-store.s3.amazonaws.com/0/316098/81909061-196f-4cf4-843b-cdf439c6064e.png)



###認証の処理
画面としては、このようなダイアログ（Firefoxの場合）が出ます。
この処理は、Javascriptの **navigator.credential.get(RPサーバーが送ったオプション)**を実行することにより出ています。
![fido-2.png](https://qiita-image-store.s3.amazonaws.com/0/316098/9a3dcf40-ae7b-4593-face-be2feeba113c.png)

RPサーバーが送ったgetオプションの例
![getoptions.png](https://qiita-image-store.s3.amazonaws.com/0/316098/33c6f6f6-da6f-dd7a-1f41-09202a3fe1c1.png)


どちらも処理結果をサーバーへPOSTする必要がありますので、以下のような感じになります。
登録時の **navigator.credentials.create** 例

```javascript

/*
* サーバーから来た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),
     })
});


```
###上記の例で送った登録時のPOSTデーター例
![clientData.png](https://qiita-image-store.s3.amazonaws.com/0/316098/c7ed0c29-7001-40bd-0033-c9754a5ab59b.png)

###認証時のPOSTデーター例
![getresponse.png](https://qiita-image-store.s3.amazonaws.com/0/316098/feddc42f-40c8-c23c-d56f-46340932fa5d.png)

##バックエンドの処理 その2
ブラウザからPOSTされたデーターを処理していきます。

###登録
登録の場合はデーターにattestationObjectが含まれます。
実行すべき処理は公開鍵情報を取り出してユーザーデーターストアに格納することです。
公開鍵情報を取り出す。と簡単に書きましたが[CBOR](http://cbor.io/)ー＞[COSE](https://tools.ietf.org/html/rfc8152)ー＞PKCSと処理します。
取り出した公開鍵を無事登録できたら、モジュールのステートは振り出しに戻り、ログイン画面へ。
黄色がブラウザ側フロントエンド　青がサーバー側バックエンド（OpenAM)です。
![fido2regseq.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316098/e9537cb9-a4a5-0e77-6496-8d41a4b2ffae.png)


###認証
認証の場合はデーターにAuthenticatorDataが含まれます。
実行すべき処理はユーザーデーターストアにある公開鍵でchallengeの署名を検証することです。
OpenAMとしては冒頭の **LOGIN_SUCCEED** までくれば認証モジュールは役割を終えます。
黄色がブラウザ側フロントエンド　青がサーバー側バックエンド（OpenAM)です。
![fido2authseq.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316098/6c5dcd17-baa3-f1eb-f97f-5fb1e8354e97.png)


###実食
コードが書けたら早速試して見るのです。
Windows Helloで試してみました。
[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/AtHIx53-Wbc/0.jpg)](https://www.youtube.com/watch?v=AtHIx53-Wbc)


##設定画面
おまけ
認証モジュールの設定画面で設定する項目はFIDO2のオプションで、サーバーとして認証時に指定したいものを選びます。
![fido2config.png](https://qiita-image-store.s3.amazonaws.com/0/316098/603cbfe5-7aaf-ef8a-12dc-e59c1697107b.png)

#これから
セルフテスト用に各種Authenticatorも買って行きたい
よさげな場所（CA or Europ)で開催される[インターオペラビリティテスト](https://fidoalliance.org/certification/interoperability-testing/)に行きたい...