【Rust】axumでセッション(cookie)の有無でルーティングを分ける

cookieを判定するmiddlewareを作成し、privateなページのみミドルウェアをつけて、publicなページにはつけない。
該当のsession_tokenがない場合は、Redirectでpublicのページにリダイレクトさせる。

async fn main() {

    let public_router = Router::new()
        .route("/setcookie", get(handle_setcookie))
        .route("/public", get(handle_public));

    let private_router = Router::new()
        .route("/test", get(handle_index))
        .route("/private", get(handle_private))
        .layer(middleware::from_fn(my_middleware));

    let app = Router::new()
        .nest("/", public_router)
        .nest("/", private_router);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
//

    if session_token == None {
        Redirect::permanent("/public").into_response()
    } else {
        let mut res = next.run(req).await;
        res
    }

おおおおおおお、昨日長時間悶絶してただけあって、ここは割と簡単に上手くいった。

【Rust】セッションをcookieに保存したい

AppendHeadersに中々辿り着けず、layerを堂々巡りした挙句、chromのcookieの設定などをミスって、これ作るのに丸2日かかりました。。。シクシク、泣きたい。

async fn handle_index()-> impl IntoResponse {
    
    let mut random = ChaCha8Rng::seed_from_u64(OsRng.next_u64());
    let mut u128_pool = [0u8; 16];
    random.fill_bytes(&mut u128_pool);
    let session_token = u128::from_le_bytes(u128_pool);
    println!("{:?}", &session_token);

    let tera = tera::Tera::new("templates/*").unwrap();

    let mut context = tera::Context::new();
    context.insert("title", "Index page");

    let output = tera.render("index.html", &context);
    let cookie:String = format!("session_token={}; Max-Age=600", &session_token);
    let headers = AppendHeaders(vec![("Set-Cookie", cookie)]);
    (headers, axum::response::Html(output.unwrap()))
}

psqlにセッションテーブルを作って、ログインしたタイミングでセッションテーブルにもsessionのvalueを保存するようにする。

【Rust】Axumでなんとかログイン機能を作りたい(その3)

axumでユーザの登録画面、insert文を作っていきます。
L formで送られてきたパスワードをハッシュ化して、usrnameと一緒に保存します。

async fn handle_save(form: Form<SignUp>)-> axum::response::Html<String> {
    let register: SignUp = form.0;
    let password_hash = hashing_password(&register.password);

    println!("{}", &password_hash);

    let _ = db_save(&register.username.to_string(), &password_hash).await.unwrap();

    let tera = tera::Tera::new("templates/*").unwrap();

    let mut context = tera::Context::new();
    context.insert("title", "Index page");

    let output = tera.render("index.html", &context);
    axum::response::Html(output.unwrap())
}

async fn db_save(name: &String, password: &String) -> Result<(), Box<dyn std::error::Error>> {
    let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=password", NoTls).await?;

    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
    client.execute(
        "INSERT INTO users (username, password) VALUES ($1, $2)",
        &[&name, &password],
    ).await?;

    for row in client.query("SELECT id, username, password From users", &[]).await? {
        let id: i32 = row.get(0);
        let name: String = row.get(1);
        let password: String = row.get(2);

        println!("{} {} {}", id, name, password);
    }
    Ok(())
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.95s
Running `target/debug/login`
$2b$08$OxMrzm88ag/iPtKahE3kveHNmmUI5uAuYGuY.lcCybGGqN8cYx/km
1 hpscript $2b$08$bKVELv/My0WaKvoyTltoLe3pOp7VJ4UxtxuA3C5qurnAblnqXbcfi
2 yamada $2b$08$OxMrzm88ag/iPtKahE3kveHNmmUI5uAuYGuY.lcCybGGqN8cYx/km

ここまでは割と簡単なんだけど、問題はここからなんだよな〜

【Rust】Axumでなんとかログイン機能を作りたい(その2)

postgres=# INSERT INTO users (username, password) VALUES (‘hpscript’, ‘$2b$08$bKVELv/My0WaKvoyTltoLe3pOp7VJ4UxtxuA3C5qurnAblnqXbcfi’);
INSERT 0 1
postgres=# select * from users;
id | username | password
—-+———-+————————————————————–
1 | hpscript | $2b$08$bKVELv/My0WaKvoyTltoLe3pOp7VJ4UxtxuA3C5qurnAblnqXbcfi

DBに入っているパスワードハッシュ値と、Postされてきたpasswordの一致を検証する。
asyncでpsqlを使う場合は、postgresではなく、tokio_postgres出ないとエラーになる。
参考: https://docs.rs/tokio-postgres/latest/tokio_postgres/

async fn handle_signup(form: Form<SignUp>)-> axum::response::Html<String> {
    let signup: SignUp = form.0;

    let hash = db_check(&signup.username.to_string()).await.unwrap();
    println!("{}", &hash);

    let password = &signup.password;
    println!("{}", verify(password, &hash));

    println!("{:?}", signup);

    // 省略
}
fn verify(password: &String, hash: &String) -> bool {
    bcrypt::verify(password, hash)
}

async fn db_check(name: &String) -> Result<String, Box<dyn std::error::Error>> {
    let (client, connection) = tokio_postgres::connect("host=localhost user=postgres password=password", NoTls).await?;

    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("connection error: {}", e);
        }
    });
    let row = client.query("SELECT * From users where username=$1", &[&name]).await?;
    let hash: String = row[0].get(2);
    println!("{}",hash);
    Ok(hash)
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.44s
Running `target/debug/login`
$2b$08$bKVELv/My0WaKvoyTltoLe3pOp7VJ4UxtxuA3C5qurnAblnqXbcfi
$2b$08$bKVELv/My0WaKvoyTltoLe3pOp7VJ4UxtxuA3C5qurnAblnqXbcfi
true
SignUp { username: “hpscript”, password: “asdf1234” }

よしよしよーし。セッションは後からやるとして、先にpsqlへのinsertとregister画面から作ります。

【Rust】uuidとbcryptパスワードハッシュの生成と検証

### uuid

use uuid::Uuid;

fn main() {
    let uuid = Uuid::new_v4().to_hyphenated().to_string();
    println!("{:?}", uuid);
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.75s
Running `target/debug/login`
“f43a4cb0-21dc-4496-8d3d-9100146f043f”

### bcrypt

use pwhash::bcrypt;

fn hashing_password(password: &String) -> String {
    bcrypt::hash(password).unwrap()
}

fn main() {
    let password = "asdf1234".to_string();
    let hash = hashing_password(&password);

    println!("{:?}", hash);
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s
Running `target/debug/login`
“$2b$08$3w9c.7T0rGYNa2iUDyzbl.qkyIsVbQp8GCWh1UZzqKNUGoAsRCE/S”

### パスワードの検証(verify)

use pwhash::bcrypt;

fn hashing_password(password: &String) -> String {
    bcrypt::hash(password).unwrap()
}

fn verify(password: &String, hash: &String) -> bool {
    bcrypt::verify(password, hash)
}

fn main() {
    let password = "asdf1234".to_string();
    let hash = hashing_password(&password);

    println!("{:?}", &hash);

    let b = verify(&password, &hash);
    println!("{:?}", &b);
}

warning: `login` (bin “login”) generated 2 warnings (run `cargo fix –bin “login”` to apply 1 suggestion)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
Running `target/debug/login`
“$2b$08$C7NvgIsgqR2Gd3/McnT0IOAYzDF1zXbZrFwNdKNrvKfm2MFqk8h.C”
true

あああ、これこれ
これで、ログイン機能もできそうな気がしてきた^^

【Rust】Argon2でパスワードハッシュを作ろう

use argon2::password_hash::{self, SaltString};
use argon2::{Argon2, PasswordHasher, Algorithm, Version, Params};

fn compute_password_hash(password: String) -> Result<String, password_hash::Error> {
    let salt = SaltString::generate(&mut rand::thread_rng());
    let password_hash = Argon2::new(
        Algorithm::Argon2id,
        Version::V0x13,
        Params::new(15000, 2, 1, None).unwrap(),
    )
    .hash_password(password.as_bytes(), &salt)?
    .to_string();
    Ok(password_hash)
}

fn main() {
    let password = "thisispassword".to_string();
    let hash = compute_password_hash(password);
    println!("{:?}", hash);
}

Compiling login v0.1.0 (/home/vagrant/dev/rust/login)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
Running `target/debug/login`
Ok(“$argon2id$v=19$m=15000,t=2,p=1$ZsxlUs1xh+gmG6VkFrCUBw$lTNPO8Dkp7yrhkRmeuVp7WzWHnf4TAm/HjRUnhQz5T8”)

なんやこれ、ますます迷走してきた…

use argon2::password_hash::{self, SaltString};
use argon2::{Argon2, PasswordHasher, Algorithm, Version, Params, PasswordHash, PasswordVerifier};

fn compute_password_hash(password: String) -> Result<String, password_hash::Error> {
    let salt = SaltString::generate(&mut rand::thread_rng());
    let password_hash = Argon2::new(
        Algorithm::Argon2id,
        Version::V0x13,
        Params::new(15000, 2, 1, None).unwrap(),
    )
    .hash_password(password.as_bytes(), &salt)?
    .to_string();
    Ok(password_hash)
}

fn verify_password_hash(
    password: String,
    expected_password_hash: String,
) -> Result<(), password_hash::Error> {
    let expected_password_hash = PasswordHash::new(expected_password_hash.as_str())?;
    Argon2::default().verify_password(password.as_bytes(), &expected_password_hash)
}

fn main() {
    let password = "thisispassword".to_string();
    let hash = compute_password_hash(password.clone());
    println!("{:?}", hash);
    verify_password_hash(password.clone(), hash.unwrap());
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
Running `target/debug/login`
Ok(“$argon2id$v=19$m=15000,t=2,p=1$SN77yEDgYTqh6QT2M9nhEw$5P53ZMpHwg0Ap/S3C2CKsZ8jjEyEPL12gscFrGf80l0”)

まぁ、そりゃそうやろ

【Rust】axumでログイン機能を作りたい

axum-loginのページをそのまま書きます。
https://docs.rs/axum-login/latest/axum_login/

use std::collections::HashMap;

use async_trait::async_trait;
use axum_login::{AuthUser, AuthnBackend, UserId};

#[derive(Debug, Clone)]
struct User {
    id: i64,
    pw_hash: Vec<u8>,
}

impl AuthUser for User {
    type Id = i64;

    fn id(&self) -> Self::Id {
        self.id
    }

    fn session_auth_hash(&self) -> &[u8] {
        &self.pw_hash
    }
}

#[derive(Clone, Default)]
struct Backend {
    users: HashMap<i64, User>,
}

#[derive(Clone)]
struct Credentials {
    user_id: i64,
}

#[async_trait]
impl AuthnBackend for Backend {
    type User = User;
    type Credentials = Credentials;
    type Error = std::convert::Infallible;

    async fn authenticate(
        &self,
        Credentials { user_id }: Self::Credentials,
    ) -> Result<Option<Self::User>, Self::Error> {
        Ok(self.users.get(&user_id).cloned())
    }

    async fn get_user(
        &self,
        user_id: &UserId<Self>,
    ) -> Result<Option<Self::User>, Self::Error> {
        Ok(self.users.get(user_id).cloned())
    }
}

fn main() {

}

ちょっと待って、最初のPassword HashのVecがわからん…
調べ物を理解するために付随するものを調べるの永遠ループが止まらん…

OpenID Connectって何?

発行者の署名付き名刺の概念を「IDトークン」と呼ぶ
IDトークンの発行者のことを「OpenIDプロバイダー」と呼ぶ
=> クライアント側の承認方式が公開鍵認証に似ている

登場人物
– ブラウザ
– クライアント: サービス提供者
– IdP: ブラウザは●●とクライアントに教える

#[derive(Debug)]
pub struct Client<P = Discovered, C: CompactJson + Claims = StandardClaims> {
    pub provider: P,
    pub client_id: String,
    pub client_secret: String,
    pub redirect_uri: Option<String>,
    pub http_client: reqwest::Client,
    pub jwks: Option<JWKSet<Empty>>,
    marker: PhantomData<C>,
}


#[tokio::main]
async fn main() {

    let client_id = std::env::var("CLIEnT_ID").expect("Unspecified CLIENT_ID as env var");
    let client_secret = std::env::var("CLIENT_SECRET").expect("Unspecified CLIENT_SECRET as env var");

    let issuer_url = std::env::var("ISSURE").unwrap_or("https://accounts.google.com".to_string());
    let issuer = reqwest::Url::parse(&issuer_url).unwrap();

    let redirect = Some(host("/login/oauth2/code/oidc"));

    let client = Arc::new(
        DiscoveredClient::discover(client_id, client_secret, redirect, issuer)
            .await
            .unwrap(),
    )
}

pub async fn authorize(
    Extension(oidc_client): Extension<Arc<OpenIdClient>>,
) -> (StatusCode, HeaderMap) {
 let origin_url = std::env::var("ORIGIN").unwrap_or(host(""));
 let auth_url = oidc_client.auth_url(&Options {
    scope: Some("openid email profile".into()),
    state: Some(origin_url),
    ..Default::default()
 });
 let url = String::from(auth_url);

 let mut headers = HeaderMap::new();
 let val = if let Ok(val) = HeaderValue::from_str(&url){
    val
 } else {
    return (StatusCode::INTERNAL_SERVER_ERROR, headers);
 };
 headers.insert(http::header::LOCATION, val);

 (StatusCode::FOUND, headers)
}

pub async fn login(
    Extension(oidc_client): Extension<Arc<OpenIDClient>>,
    login_query: Query<LoginQuery>,
) -> impl IntoResponse {
    let request_token = request_token(oidc_client, &login_query).await;
    match request_token {
        Ok(Some((token, user_info))) => {
            let login = user_info.preferred_username.clone();
            let email = user_info.email.clone();

            let user = Usr {
                id: usr_info.sub.clone().unwrap_or_default(),
                login,
                last_name: usr_info.family_name.clone(),
                first_name: user_info.name.clone(),
                email,
                activated: user_info.email_verified,
                image_url: user_info.picture.clone().map(|x| x.to_string()),
                lang_key: Some("en".to_string()),
                authorities: vec!["ROLE_USER".to_string()],
            };
            //...
        }
    }
}

async fn request_token(
    oidc_client: Arc<OpenIDClient>,
    login_query: &LoginQuery,
) -> anyhow::Result<Option<(Token, Userinfo)>> {
    let mut token: Token = oidc_client.request_token(&login_query.code).await?.into();

    if let Some(mut id_token) = token.id_token.as_mut() {
        oidc_client.decode_token(&mut id_token)?;
        oidc_client.validate_token(&id_token, None, None)?;
    } else {
        return Ok(None);
    }
    let userinfo = oidc_client.request_userinfo(&token).await?;

    Ok(Some((token, userinfo)))
}

OpenID Connectの全体像は大体わかったんだが、コレジャナイ感が強い。

Laravelでログインユーザーのみページを表示させる

# /resources/viewsの下にmenu.blade.phpをつくる。適当にhelloとしておく

# 続いて、/app/Http/Controllersに MenuController.phpをつくる

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class MenuController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('menu');
    }
}

# /routes/web.phpにroutingを追加

Route::get('/home', 'MenuController@index')->name('menu');

# ログインしている時

# ログアウトしている時は自動的にloginにリダイレクトされる

おおおおおおおおおおおおおお、なるほど、ようは、controllerで$this->middleware(‘auth’);があればいいのね。なるほど。ログインサービス作りたくなってきた。

laravelのログイン認証を勉強しよう

# ディレクトリを作成します
[vagrant@localhost app]$ mkdir angel

# 作成したディレクトリに移動
[vagrant@localhost app]$ cd angel
[vagrant@localhost angel]$ ls

# composerをインストール
[vagrant@localhost angel]$ curl -sS https://getcomposer.org/installer | php
[vagrant@localhost angel]$ ls
composer.phar

# composerでlaravelをインストール。ディレクトリ名は適当にangelとしておきます。
php composer.phar create-project –prefer-dist laravel/laravel angel

# mysqlにログイン
[vagrant@localhost ~]$ mysql -u root -p
Enter password:
mysql> show databases;

# mysqlでDBの作成
mysql> create database angel;
Query OK, 1 row affected (0.12 sec)

# .envファイルを編集

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=angel
DB_USERNAME=root
DB_PASSWORD=secret

# migration 実行
[vagrant@localhost angel]$ cd angel
[vagrant@localhost angel]$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table

# 作成されたtable確認
mysql> use angel;
mysql> show tables;
+—————–+
| Tables_in_angel |
+—————–+
| migrations |
| password_resets |
| users |
+—————–+
3 rows in set (0.00 sec)

mysql> describe migrations;
+———–+——————+——+—–+———+—————-+
| Field | Type | Null | Key | Default | Extra |
+———–+——————+——+—–+———+—————-+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| migration | varchar(255) | NO | | NULL | |
| batch | int(11) | NO | | NULL | |
+———–+——————+——+—–+———+—————-+
3 rows in set (0.08 sec)

mysql> describe password_resets;
+————+————–+——+—–+———+——-+
| Field | Type | Null | Key | Default | Extra |
+————+————–+——+—–+———+——-+
| email | varchar(255) | NO | MUL | NULL | |
| token | varchar(255) | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
+————+————–+——+—–+———+——-+
3 rows in set (0.00 sec)

mysql> describe users;
+——————-+———————+——+—–+———+—————-+
| Field | Type | Null | Key | Default | Extra |
+——————-+———————+——+—–+———+—————-+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | UNI | NULL | |
| email_verified_at | timestamp | YES | | NULL | |
| password | varchar(255) | NO | | NULL | |
| remember_token | varchar(100) | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+——————-+———————+——+—–+———+—————-+
8 rows in set (0.00 sec)

# 認証機能を生成
[vagrant@localhost angel]$ php artisan make:auth
Authentication scaffolding generated successfully.

# routes/web.phpに追加されている

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

# php artisan serveでサーバを立てる
[vagrant@localhost angel]$ php artisan serve –host 192.168.35.10 –port 8000
Laravel development server started:

# ${domain}/register にアクセス

おおおおおおおおおお、これはちょっと勉強が必要だ。