RustでLINEログイン認証のPKCE対応をした話

## 前提
- "LINE Developers"や"LINE Official Management Account"などへの登録は事前に済ませているものとする


- LINEのコールバックURIの登録や、アカウントの作成は済んでいるものとする。

---
## 背景
LINEログインの認証にPKCEが対応された

→　既存のログインにPKCE対応を行うことにしよう！

---
## PKCEとは
- PKCEとはProof Key for Code Exchangeの略で[RFC7636](https://datatracker.ietf.org/doc/html/rfc7636)として公開された仕様


- 認可コード横取り攻撃への対応策として使われる

---
## PKCEの仕様
- "code_verifier"と"code_challenge"という二つの値を使う
- "code_verifier"から"code_challenge"を生成する生成器を使用する
- 生成器は"code_verifier"をSHA256でハッシュ化し、Base64URL形式にエンコードした値を出力する
- 同じ"code_verifier"から同じ"code_challenge"を生成することができるが、逆は生成不可である

---
## 認可コード横取り攻撃、PKCE未実装時
![未実装.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1770714/0658824a-1b37-7c66-23ea-ee742a4e9ea2.png)

---
## 認可コード横取り攻撃、LINE PKCE実装時
![実装時.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1770714/43b11452-4fbd-4d32-8196-37529adc3f66.png)

---
## LINEログインのイメージと対応箇所
![PKCE対応箇所.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1770714/0ddd4dc4-c440-340c-caca-11528d898c3c.png)

---
## 実装
- 今回の実装は、code_verifierとcode_challengeの生成、認可後のアクセストークンの送信はrustで行う
- code_verifierとcode_challengeの生成は'[pkce-Rust](https://crates.io/crates/pkce)'というCrateを使用する
- code_verifierがバイナリーであるため、LINEログインで使用するためにcharに変換を行う

---
## code verifierの生成（クレートから引用）
```rust:pkce-rs
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
    abcdefghijklmnopqrstuvwxyz\
    0123456789-.~_";

pub fn code_verifier(length: usize) -> Vec<u8> {
    let mut rng = thread_rng();

    (0..length)
        .map(|_| {
            let i = rng.gen_range(0, CHARS.len());
            CHARS[i]
        })
        .collect()
}
```

--- 
## code challengeの生成（クレートから引用）
```rust:pkce-rs
pub fn code_challenge() {
    // ハッシュ化
    let mut sha = Sha256::new();
    sha.update(code_verifier);
    let result = sha.finalize();

    // base64でエンコード
    base64_url_encode(&result[..])
}
```

---
## 実際にLINEログインに使用する値
```rust:generate_verifier_and_challenge
fn generate_verifier_and_challenge() -> (String, String) {
    // 文字数の最小値と最大値(LINEの仕様)
    let char_size_min = 43;
    let char_size_max = 128 + 1;
    let rng = &mut thread_rng();
    let size = rng.gen_range(char_size_min..char_size_max);

    // code_verifierとcode_challengeの生成
    let u8_verify = pkce::code_verifier(size);
    let code_verifier = u8_verify.iter().map(|&s| s as char).collect::<String>();
    let code_challenge = pkce::code_challenge(&u8_verify);
    (code_verifier, code_challenge)
}
```

---
## LINEログインの認可URIにcode_challengeを組み込む
```rust:make_redirect_uri
fn make_redirect_uri() {
    let pkce = generate_verifier_and_challenge();

    let redirect_uri = format!("https://access.line.me/oauth2/v2.1/authorize?scope=profile&prompt=consent&bot_prompt=normal&response_type=code&client_id=xxxxx&redirect_uri=https://ussy.index.html&state=xxxx&code_challenge={}&code_challenge_method=S256", pkce.1);
}
```
```rust:認可URI
https://access.line.me/oauth2/v2.1/authorize?scope=profile&prompt=consent&bot_prompt=normal&response_type=code&client_id=xxxxx&redirect_uri=https://ussy.index.html&state=xxxx&code_challenge=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&code_challenge_method=S256
```

---
## LINEログインのOauth認証の仕様
- 認可URIの最後にcode_challengeとcode_challenge_methodを追加することでPKCE対応を行うことができる
- code_challengeがついていないURIについては、通常のPKCE対応を行っていない認可URIとみなされる（LINE Developers）

--- 
## アクセストークンの発行
- 認可URIにcode_challengeを付与したので、アクセストークンのリクエストにcode_verifierの情報を付与する
- code_verifierは例えばDBなどに保存する
- 今回はwebアプリの想定し、SessionStorageを用いてcode_verifierを保存した
- 保存したcode_verifierをRust側に渡し、アクセストークンのリクエストを作成する

---
## アクセストークンの発行
```rust:request_bodyの送信部分
    let request_body: Vec<(&str, &str)> = vec![
        ("grant_type", "authorization_code"),
        ("code", code),
        ("redirect_uri", redirect_uri),
        ("client_id", client_id),
        ("client_secret", client_secret),
        ("code_verifier", code_verifier),
    ];

    let client = reqwest::Client::new();
    let res = client
        .post(
            "https://api.line.me/v2/oauth/accessToken"
        )
        .form(&params)
        .send()
        .await?;
```

---
## まとめ
- LINEログインのPKCE対応で以下を行った
    - Rustでcode_verifierとcode_challengeの生成部分の実装
    - redirect_uriとaccess_token_request作成部分の実装

- 他のサービスでも、Oauth2.0拡張のPKCEに対応しているものであれば、そのサービスの仕様に基づいて同様に実装できる



---
## 参考資料
- [RFC7636](https://datatracker.ietf.org/doc/html/rfc7636)
- [LINE Developers](https://developers.line.biz/ja/docs/line-login/integrate-pkce/)
- [pkce-Rust](https://crates.io/crates/pkce)