Rustのactix-webで作成されたセッションCookieの中身を調べてみる

# はじめに

RustのWebフレームワークの一つであるactix-webを使って発行されたセッションCookieの中身を調べてみました。
本記事ではactix-webと組み合わせて使うactix-sessionのオブジェクトを使ってキーと値を生成し、その情報をセッションCookieに格納してクライアントに返す実装例を記載します。クライアントがセッションCookieを受け取った後、そこからactix-sessionで作ったキーと値を復元するのが目標です。

過去にPythonのFlaskで同様のことを行っており以下記事に書いていますが、今回はそのRust版です。

https://qiita.com/showchan33/items/b714cca80985b3db2565

# 動作確認バージョン

```
$ rustc -V
rustc 1.73.0 (cc66ad468 2023-10-03)
$ cargo -V
cargo 1.73.0 (9c4383fb5 2023-08-26)
```

# Webサーバの作成

まずはセッションCookieを生成するWebサーバを作ります。

## Cargo.tomlにパッケージ追記

actix-webとactix-sessionのパッケージが必要なため、``Cargo.toml``の``[dependencies]``セクションに以下のように記載します。

```toml:Cargo.toml
[package]
name = "actix-web-cookie-test"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.6.0"
actix-session = { version = "0.8.0", features=["cookie-session"] }
```

※ 今回セッションCookieを発行するのに[actix-session::storage::CookieSessionStore](https://docs.rs/actix-session/latest/actix_session/storage/struct.CookieSessionStore.html)を使うため、``cookie-session``のfeatureが追加で必要となります。

## Webサーバ実装

次の機能を持つWebサーバを実装します。

* Webクライアントが「user_idのキーを格納したセッションCookie」を持っているかを確認
    * 持っている場合は何もしない
    * 持っていなければ、キーがuser_idで値が1のセッションCookieを発行する

以下がそのソースです。

```rs:main.rs
use actix_web::{ cookie::Key, App, HttpServer, get, HttpResponse };
use actix_session::{ Session, SessionMiddleware, storage::CookieSessionStore };
use actix_session::config::CookieContentSecurity::Private;

/// セッションCookieを発行するWebサーバのpathとその処理を記載
#[get("/")]
async fn root(
  session: Session,
) -> HttpResponse {

  let mut response_text = String::new();

  if let Some(_) = session.get::<String>("user_id").unwrap() {
    response_text.push_str("Session cookie accepted.\n");
  } else {
    session.insert("user_id", "1").unwrap();
    response_text.push_str("No user_id key in session.\nSession cookie created.\n");
  }

  HttpResponse::Ok().body(response_text)
}

/// セッションCookieの作り方について記載（後で解説）
fn session_middleware(
  secret_key: &str,
) -> SessionMiddleware<CookieSessionStore> {
  SessionMiddleware::builder(
    CookieSessionStore::default(),
    Key::from(secret_key.as_bytes()),
  )
  .cookie_name(String::from("mycookie"))
  .cookie_content_security(Private)
  .cookie_secure(false)
  .build()
}

#[actix_web::main]
async fn main() -> Result<(), actix_web::Error> {

  // セッションCookieを作成する際のシークレットキー。64バイト以上である必要あり
  let secret_key = "secret_key".repeat(7);

  HttpServer::new(move ||
    App::new()
    .wrap(session_middleware(&secret_key)) // 後で解説
    .service(root)
  )
  .bind("0.0.0.0:3000")?
  .run()
  .await?;

  Ok(())
}
```

### actix-webのミドルウェアを作成している部分についての解説

``session_middleware``関数では、セッションCookieを生成するためのミドルウェアである[SessionMiddleware](https://docs.rs/actix-session/latest/actix_session/struct.SessionMiddleware.html)のオブジェクトを作成しています。SessionMiddlewareオブジェクトは、actix-webのHttpServerでWebサーバを起動する際にwrap関数によって組み込まれます。

このactix-webのミドルウェアが具体的に何なのか？私は最初ピンと来なかったのですが、調べていくと、リクエストやレスポンスをする際に追加の処理を入れられるものだということがわかりました。公式ドキュメントは以下です。

https://actix.rs/docs/middleware/

今回の場合だとSessionMiddlewareのオブジェクトが、セッションCookieを作成したり、リクエストで届いたセッションCookieを検証したりするところを一手に引き受けてくれます。 SessionMiddlewareが具体的にどのような処理をしているのかは、以下のソースに書いてあるようです。ご興味のある方はぜひ解析してみてください。

https://docs.rs/actix-session/latest/src/actix_session/middleware.rs.html

SessionMiddlewareが色々とやってくれるので、我々が実装するWebサーバのソースではセッションCookieを作るために必要な情報だけ渡せば済みます。上記の``main.rs``のソースだと``root``関数内の``session``というオブジェクト（[actix_session::Session](https://docs.rs/actix-session/latest/actix_session/struct.Session.html)型）がこれに相当して、セッションCookieに含めるキーと値を格納しています。SessionMiddlewareはこの``session``に格納された情報をもとにクライアントに返すセッションCookieを作ったり、クライアントから受け取ったセッションCookieから自分で作ったキーと値を抽出して``session``に格納する処理を行ったりしています。

:::note info
以前に書いた[Python FlaskのセッションCookie調査](https://qiita.com/showchan33/items/b714cca80985b3db2565#%E4%B8%80%E3%81%A4%E7%9B%AE%E3%81%AFkeyvalue%E3%81%8C%E3%83%9A%E3%82%A4%E3%83%AD%E3%83%BC%E3%83%89%E3%81%A8%E3%81%97%E3%81%A6%E6%A0%BC%E7%B4%8D)の場合、セッションCookieを生成する機能がFlask自体にsessionオブジェクトとして提供されていました。しかしRustのactix-webには、既にお察しのようにそのような機能がありません。代わりにactix-sessionが、セッションCookieをいい感じで生成してくれます。
:::

### SessionMiddlewareBuilderについて

SessionMiddlewareのオブジェクトは、[SessionMiddlewareBuilder](https://docs.rs/actix-session/latest/actix_session/config/struct.SessionMiddlewareBuilder.html)を使って作られ、セッションCookieの作成に関する様々な設定が行えます。本記事のソース ``main.rs``では、次の設定を行っています。

```rs
  .cookie_name(String::from("mycookie")) // Cookie名を"mycookie"にする
  .cookie_content_security(Private)      // セッションCookieを暗号化する
  .cookie_secure(false)                  // httpのリクエストでもセッションCookieを作れるようにする
```

## Webサーバを起動して、クライアントからセッションCookieを取得できることを確認

``cargo run``コマンド等でWebサーバを起動します。
Webサーバ起動後にクライアントから``curl``を実行すると、次のようにセッションCookieが送られているのを確認できます。

```
$ curl -i http://127.0.0.1:3000/
HTTP/1.1 200 OK
content-length: 51
set-cookie: mycookie=B%2FujNXu7ZsV+2TTriWdxSpwu1+9Fd%2FODUdEBDny+4Bzcey0axtqGvZEGT+D6jWU%3D; HttpOnly; SameSite=Lax; Path=/
date: Sat, 25 May 2024 06:36:44 GMT

No user_id key in session.
Session cookie created.
```

### 余談 : CookieContentSecurityの設定を変えた場合の挙動

SessionMiddlewareBuilderの``cookie_content_security``関数で、引数を``Private``ではなく``Signed``とした場合、次のようにセッションCookieに``user_id:1``のキーと値が暗号化されずに直接格納されます。

```
$ curl -i http://127.0.0.1:3000/ 2>&1 | grep ^set-cookie
set-cookie: mycookie=7pwihAkZObaaW26QBIpMi5OYp2WP2aFUBx7Q%2FCWSQek%3D%7B%22user_id%22%3A%22%5C%221%5C%22%22%7D; HttpOnly; SameSite=Lax; Path=/
```

以下のCookieContentSecurityにも書かれていますが、セッションCookieの内容を秘匿化したい場合は``Private``、中身がクライアントに見えても問題ないので改ざんを防ぎたい場合は``Signed``で署名をする、という使い分けをするようです。

https://docs.rs/actix-session/latest/actix_session/config/enum.CookieContentSecurity.html

※さらに余談になってしまいますが、セッションCookieにはセッション維持のために必要な最低限の情報を格納し、それ以外のセッションに関する情報はRedis等を使ってサーバー側に持たせる「サーバーサイドセッション管理」という方法もあります。この場合だと要件にもよると思いますが、秘匿化はせずに署名のみで事足りるケースもあるかもしれません。

# セッションCookieの中身を調査

先ほどcurlコマンドで、``CookieContentSecurity``が``Private``の場合に出力されたセッションCookieの値は以下でした。
```
B%2FujNXu7ZsV+2TTriWdxSpwu1+9Fd%2FODUdEBDny+4Bzcey0axtqGvZEGT+D6jWU%3D
```
パッと見る限り少なくともURLエンコードされているのはわかりますが、それ以外はどうなっているのかさっぱりわからないです（暗号化されているので当然ですが...）。以前に[Python FlaskのセッションCookie調査](https://qiita.com/showchan33/items/b714cca80985b3db2565#%E4%B8%80%E3%81%A4%E7%9B%AE%E3%81%AFkeyvalue%E3%81%8C%E3%83%9A%E3%82%A4%E3%83%AD%E3%83%BC%E3%83%89%E3%81%A8%E3%81%97%E3%81%A6%E6%A0%BC%E7%B4%8D)でやったようなヤマ勘はとても無理そう。一体どうすればこの中身がわかるのだろうか...

先ほど、SessionMiddlewareの機能の一つとして、「クライアントが送られたセッションCookieから自前で作成したキーと値を抽出する」ものがあると述べました。ということは、SessionMiddlewareのソースを参考に実装したら、上の不可解な文字列から意味のある情報を抽出できそうです。

改めてソースを見てみると、[extract_session_key](https://docs.rs/actix-session/latest/src/actix_session/middleware.rs.html#325)というドンピシャな名前の関数がありました。これをもとに実装してみたのが以下です。

```rs:main.rs
use cookie::{Cookie, Key, CookieJar};

fn main()  {
  // curlのset-cookieの結果をそのままコピペ
  let cookie_str = "mycookie=B%2FujNXu7ZsV+2TTriWdxSpwu1+9Fd%2FODUdEBDny+4Bzcey0axtqGvZEGT+D6jWU%3D; HttpOnly; SameSite=Lax; Path=/";

  let cookie = Cookie::parse_encoded(cookie_str).unwrap();

  let secret_key_str = "secret_key".repeat(7);
  let secret_key = Key::from(secret_key_str.as_bytes());

  let mut jar = CookieJar::new();
  jar.add_original(cookie.clone());

  // secret_keyでmycookieという名前のセッションCookieを復号
  let private_cookie = jar.private(&secret_key).get("mycookie");
  println!("{:?}", private_cookie.unwrap().value());
}
```

上記のソースを動かすには、``Cargo.toml``で以下のようにパッケージを記載する必要があります。

```toml:Cargo.toml
[package]
name = "extract-session-cookie"
version = "0.1.0"
edition = "2021"

[dependencies]
cookie = { version = "0.16.0", features=["private", "percent-encode"] }
```

ビルドして実行すると、次のようにサーバ側で格納した``"user_id"``と``"1"``のキーと値を見事に取得できました！
```
$ cargo run
   Compiling extract-session-cookie v0.1.0 (/path/to/extract-session-cookie)
    Finished dev [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/extract-session-cookie`
"{\"user_id\":\"\\\"1\\\"\"}"
```

ちなみに、セッションCookieを復号する以下に関してですが、
```rs
  // secret_keyでmycookieという名前のセッションCookieを復号
  let private_cookie = jar.private(&secret_key).get("mycookie");
```

以下のソースに暗号化(encrypt_cookie)と復号(unseal)の処理が書かれていました。

https://github.com/rwf2/cookie-rs/blob/master/src/secure/private.rs#L42