FedCM(Federated Credential Management API)を実装して動かしてみた


# FedCMの現状把握

WebIDから名前が変わったFedCM(Federated Credential Management API)について、現状のステータスや実装を見ていきたいと思います。

FedCMの背景などについては以前OpenID TechNight vol.17でお話しした際の資料があるのでそちらもご参考いただけると助かります。

https://www.slideshare.net/techblogyahoo/bounce-tracking-protectionfederation-openid-technight

## FedCMの参照先
FedCMの仕様や実装状況を追う上で参考にしたものは下記になります。

- https://github.com/WICG/FedCM
    - https://github.com/WICG/FedCM/blob/main/explainer/HOWTO.md
- https://wicg.github.io/FedCM/
- https://github.com/chromium/chromium
    - エラー発生時の原因調査、最新の実装状況把握のため

## 今回の確認内容
FedCMでは下記の3つのフローが存在しますが、まだDelegation oriented Flowは実装されていないようなため、Permission oriented FlowとMediated oriented Flowについて、正常系を動かすところまでやってみたいと思います。

- Permission oriented Flow
- Mediated oriented Flow
- ~~Delegation oriented Flow~~

## 動作確認環境
https://github.com/WICG/FedCM/blob/main/explainer/HOWTO.md

まずはこちらのドキュメントに従って、FedCMが動作する環境を準備します。

実際に動作を確認したChrome Canaryのバージョンは下記になります。
明日には仕様が変わっているかもしれませんので、現時点の実装状況であることをご了承ください。

|macOS|Android|
|:---|:---|
|98.0.4758.2|99.0.4759.0|

### Chrome CanaryでFedCMを有効化
Chrome Canaryのアドレスバーで`chrome://flags/#fedcm` と入力すると、FedCMの機能を有効化するフラグを立てることができます。
左側のボタンが「Default」となっているので、これを「Enabled」に変更します。
![Screen Shot 2021-12-10 at 21.35.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65163/7df72ece-d22a-7229-66f3-86238a433bb6.png)

以上で、動作確認環境の準備は完了です。

次に実装をしていきます。

## Permission oriented Flow
https://github.com/WICG/FedCM/blob/main/explainer/HOWTO.md

上記を見ながら、実装と処理の流れを追っていきます。

このフローはPCでしか動作確認できなかったのでご注意ください。

まずはRPからID Tokenのリクエストを行います。
`navigator.credentials.get`の`providers`にはIdPの情報を、`mode`はPermission oriented Flowを示す`permission`を指定します。

```javascript
const token = await navigator.credentials.get({
  federated: {
    providers: [{
      // IdPのURL
      url: "https://idp.example.com",
      // RPのClient ID
      clientId: "client_id",
      // nonce値
      nonce: "456"
    }],
    // Permission oriented Flowの指定
    mode: "permission"
  }
});
```

ID Tokenのリクエストを行うと、ユーザに対して説明のためのプロンプトが表示されます。
少しわかりづらいですが、`nebula-dynamic-lynx.glitch.me` はIdPのドメインで、`artistic-roasted-sting.glitch.me`がRPのドメインです。

![Screen Shot 2021-12-13 at 7.50.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65163/ae99d871-d803-36bc-381e-dffc7ea85d26.png)

ここで、「Continue」を押すと、後続の処理が実行されます。

ブラウザが`providers`で指定したIdPの`.well-known/webid`へリクエストをし、後続のフローで利用するエンドポイントを取得します。
ちなみにドキュメント上では`.well-known/fedcm`となっていますが、現状の実装は`.well-known/webid`へリクエストが来ているようでした。

```json
{
  "idp_endpoint": "https://idp.example/fedcm/idp_endpoint"
}
```

このあとブラウザは`idp_endpoint`へリクエストを行い、ID Tokenの取得を試みます。
この`idp_endpoint`はOAuthのAuthorization エンドポイントと互換性を保つように設計される想定となっているようで、scopeやclient_id、nonceなどのパラメータが付与されます。

現状はclient_idとnonceのみ指定した値が使われ、scopeは`openid profile email`が固定されています。
https://github.com/chromium/chromium/blob/016a05172b832baff78e418e51ab738257438c3c/content/browser/webid/federated_auth_request_impl.cc#L33-L47

IdPは`idp_endpoint`へのリクエストに対して、ユーザーの状況に応じていずれかのレスポンスを返却します。

もし、ID Tokenを返却できる状況であれば、ID Tokenを返します。
これでRPに対してID Tokenが返却されID Tokenのリクエストが完結します。

```json
{
  "id_token": "eyJ..."
}
```

もし、ID Tokenを返却できない状況であれば、認証などを行うためのURLをブラウザに提供します。

```json
{
  "signin_url": "https://idp.example/fedcm/user_login"
}
```

ここで返却されたURLに対して、ブラウザが特別なUIと共にユーザーに明示的に同意を取得します。

![Screen Shot 2021-12-13 at 8.23.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65163/a6e4aa4a-427e-07ee-49df-c24957e42299.png)

![Screen Shot 2021-12-13 at 8.24.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65163/f73e783a-322a-4368-36bb-51d98d3d26f2.png)

最後にIdPは`navigator.id.provide()`呼び出すことでID TokenをRP返却することが可能となります。

```javascript
navigator.id.provide("eyJ...");
```

以上がPermission oriented Flowの正常系の処理となります。

## Mediated oriented Flow
https://github.com/WICG/FedCM/blob/main/explainer/HOWTO.md

上記を見ながら、実装と処理の流れを追っていきます。

このフローはAndroidでしか動作確認できなかったのでご注意ください。

まずはRPからID Tokenのリクエストを行います。
`navigator.credentials.get`の`providers`にはIdPの情報を、`mode`はMediated oriented Flowを示す`mediated`を指定します。

デフォルト値も`mediated`となっているため、指定しなければMediated oriented Flowとなります。

```javascript
const token = await navigator.credentials.get({
  federated: {
    providers: [{
      // IdPのURL
      url: "https://idp.example.com",
      // RPのClient ID
      clientId: "client_id",
      // nonce値
      nonce: "456"
    }],
    // Mediated oriented Flowの指定
    mode: "mediated"
  }
});
```

ブラウザが`providers`で指定したIdPの`.well-known/webid`へリクエストをし、後続のフローで利用するエンドポイントを取得します。
Mediated oriented Flowに関しても現状の実装は`.well-known/webid`へリクエストが来ているようでした。

```json
{
  "idtoken_endpoint": "https://idp.example/fedcm/token_endpoint",
  "accounts_endpoint": "https://idp.example/fedcm/accounts_endpoint",
  "client_id_metadata_endpoint": "https://idp.example/fedcm/client_id_metadata_endpoint"
}
```

まずは`accounts_endpoint`へリクエストがされ、IdPのアカウント一覧をレスポンスで返却します。

```json
{
 "accounts": [{
    "sub": "1234",
    "name": "John Doe",
    "given_name": "John",
    "email": "john_doe@idp",
    "picture": "https://idp.example/profile/123",
  }, {
    "sub": "5678",
    "name": "Johnny",
    "given_name": "Johnny",
    "email": "johnny@idp",
    "picture": "https://idp.example/profile/456"
  }
 ]
}
```

レスポンスを返すことができると、ブラウザのUIとしてアカウントチューザーが表示されます。

スクリーンショットは取れなかったため、イメージは以下のようになります。

`hallowed-tattered-split.glitch.me` はIdPのドメインで、`artistic-roasted-sting.glitch.me`がRPのドメインです。

![Screen Shot 2021-12-13 at 9.09.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65163/fd01d52e-3f41-a2af-2ff0-e5a421a8c2ad.png)

いずれかのアカウントを選択すると、下記のような画面が表示され、ID連携をするための同意を取得します。

![Screen Shot 2021-12-13 at 9.17.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65163/860e6589-4cc5-783b-2447-11fe1eb8498e.png)

この際に表示する「privacy policy」や「terms of service」のリンクは`client_id_metadata_endpoint`のレスポンスから取得しています。

```json
{
  "privacy_policy_url": "https://rp.example/privacy",
  "terms_of_service_url": "https://rp.example/terms"
}
```

ここで「Continue as John」を押すと、`idtoken_endpoint`へリクエストが発生します。

ドキュメント上では、下記のリクエストボディとなっていますが、

```json
{
  "account": 1234,
  "request_id": "xyz123123zyx",
  "request": {
    "client_id": "myclientid",
    "nonce": "abc987987cba"
  }
}
```

現状は下記のようなリクエストボディとなっていました。

```json
{
  "request": "scope=openid profile email&client_id=client_id&nonce=456",
  "sub": "1234"
}
```

いずれにせよ、Authorizationエンドポイントと互換性のあるパラメータと、選択した識別子が渡ってくると思われます。

IdPは`idtoken_endpoint`では下記のようなレスポンスを返すことで、RPにID Tokenを返すことができます。

```
{
  "id_token": "eyJ..."
}
```

以上がMediated oriented Flowの正常系の処理となります。

## 実装
一応動作確認のために実装した内容は下記となっているので、とりあえず動くものという感じですが、お好きに試してみたり、コードを見てもらえればと思います。

### RP

- 動作ページ
    - https://artistic-roasted-sting.glitch.me
- コード
    - https://glitch.com/edit/#!/artistic-roasted-sting

### IdP(Permission oriented)
- 動作ページ
    - https://nebula-dynamic-lynx.glitch.me
- コード
    - https://glitch.com/edit/#!/nebula-dynamic-lynx

### IdP(Mediated oriented)
- 動作ページ
    - https://hallowed-tattered-split.glitch.me
- コード
    - https://glitch.com/edit/#!/hallowed-tattered-split

## 最後に
Permission oriented FlowとMediated oriented Flowに関しては、まだドキュメントと実装が乖離している箇所もありつつ、正常系の動作は動かせるような状況となっていました。

仕様と実装が乖離している場合、chromiumのコードを参照すると最新の実装状況を把握できるため、エラー文言などを検索するなどして実装を進められました。

また、Permission oriented FlowはPC、Mediated oriented FlowはAndroid端末で動作確認可能で、デモ実装の紹介も行ったので、ぜひ手元で試してみていただければと思います。

以上、引き続き余裕があれば記事を更新していこうと思うので、引き続きよろしくお願いいたします。

最後に、記事の投稿が遅れてしまい申し訳ありませんでした:bow: