【Rust】ChaCha8Rngでセッションを作りたい

use rand_core::OsRng;
use rand_core::RngCore;
use rand_chacha::ChaCha8Rng;
use rand_chacha::rand_core::SeedableRng;


fn main(){
    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);
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/sample`
68291255163114295989027254289217915346

いまいち意味わかってないけどすげえ…

【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】Axumでなんとかログイン機能を作りたい(その1)

### usersテーブルの作成

sudo -u postgres psql

CREATE TABLE users (
	id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
	username text NOT NULL UNIQUE,
	password text NOT NULL
);

axumでformでpostされたデータを受け取ります。

use axum::{Router, routing::get, routing::post, extract::Form};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Clone, Debug)]
struct SignUp {
    username: String,
    password: String,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", axum::routing::get(handle_index))
        .route("/signup", axum::routing::post(handle_signup));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn handle_index()-> axum::response::Html<String> {
    let tera = tera::Tera::new("templates/*").unwrap();

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

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

async fn handle_signup(form: Form<SignUp>)-> axum::response::Html<String> {
    let signup: SignUp = form.0;
    println!("{:?}", signup);

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

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

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

コマンドラインからPostします。
$ curl -X POST -d ‘username=山田太郎’ -d ‘password=asdf1234’ 127.0.0.1:3000/signup

Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.65s
Running `target/debug/login`
SignUp { username: “山田太郎”, password: “asdf1234” }

なるほど、なんとかここまでできた。ミドルウェアのところと、セッションの箇所をどうするか悩ましいが、まぁよしとしよう。

【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の全体像は大体わかったんだが、コレジャナイ感が強い。

【Rust】axumのauthとmiddleware

use std::{collections::HashMap, sync::Arc};

use axum::{
    async_trait,
    extract::{FromRequestParts, Request, State},
    middleware::Next,
    response::Response,
    RequestExt as _,
};
use axum_extra::{
    headers::{authorization::Bearer, Authorization},
    TypedHeader,
};
use http::{request::Parts, StatusCode};

pub type Token = String;
pub type UserMap = HashMap<Token, User>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct User {
    pub username: String,
}

pub fn build_user_map() -> UserMap {
    let mut user_map = HashMap::new();
    user_map.insert(
        "aaa".to_string(),
        User {
            username: "Andy".to_string(),
        },
    );
    user_map.insert(
        "bbb".to_string(),
        User {
            username: "Bella".to_string(),
        },
    );
    user_map.insert(
        "ccc".to_string(),
        User {
            username: "Callie".to_string(),
        },
    );
    user_map.insert(
        "ddd".to_string(),
        User {
            username: "Daren".to_string(),
        },
    );
    user_map
}

#[async_trait]
impl<S> FromRequestParts<S> for User
where
    S: Send + Sync,
{
    type Rejection = StatusCode;

    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
        let user = parts
            .extensions
            .get::<Self>()
            .expect("User not found. Did you add auth_middleware?");
        Ok(user.clone())
    }
}

pub async fn auth_middleware(
    State(user_map): State<Arc<UserMap>>,
    mut request: Request,
    next: Next,
) -> axum::response::Result<Response> {
    let bearer = request
        .extract_parts::<TypedHeader<Authorization<Bearer>>>()
        .await
        .map_err(|_| StatusCode::BAD_REQUEST)?;
    let token = bearer.token();

    let user = user_map.get(token).ok_or(StatusCode::UNAUTHORIZED)?;
    request.extensions_mut().insert(user.clone());

    Ok(next.run(request).await)
}
mod auth;

use std::sync::Arc;
use axum::{middleware::from_fn_with_state, routing::get, Router};
use auth::{auth_middleware, build_user_map, User, UserMap};


#[tokio::main]
async fn main() {
    let user_map = build_user_map();
    let app = build_app(Arc::new(user_map));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

#[rustfmt::skip]
fn build_app(user_map: Arc<UserMap>) -> Router {
    let public_router = Router::new()
        .route("/public", get(public));

    let private_router = Router::new()
        .route("/private", get(private))
        .route("/your-name", get(your_name))
        .route_layer(from_fn_with_state(user_map.clone(), auth_middleware));

    Router::new()
        .nest("/", public_router)
        .nest("/", private_router)
        .with_state(user_map)
}

async fn public() -> &'static str {
    "This is public."
}

async fn private() -> &'static str {
    "This is public."
}

async fn your_name(user: User) -> String {
    format!("Your name is {}.", user.username)
}

curl http://192.168.33.10:3000/public
curl -I http://192.168.33.10:3000/private
curl -H ‘Authorization: Bearer aaa’ http://192.168.33.10:3000/your-name

やりたいことはギリわかるが、Bearer認証およびtraitのextensionとauth middlewareのinputとその先の内容がイマイチ理解できん…

【Rust】各ブロックチェーンの対BTCスワッピングレートを計算して表示

各コインのUSDレートをbitcoinのUSDレートで割れば、スワッピングレートが計算される。
f32のvectorはpartial_cmpで比較してソートする。

let mut coin_rate = get_price().await.unwrap();

    let objs: Vec<Rate> = serde_json::from_value(coin_rate).unwrap();
    let mut btcRate: f32 = 0.0;

    let mut crypt_objs: Vec<Rate> = Vec::new();
    for obj in objs {
        if obj.r#type == "crypto" {
            if obj.symbol == "BTC" {
                crypt_objs.insert(0, obj.clone());
                btcRate = obj.rateUsd.parse::<f32>().unwrap();
            } else {
                crypt_objs.push(obj);
            }   
        }
    }
    println!("{}", &btcRate);

    let mut data: Vec<RateSwap> = vec![];
    for crypt_obj in crypt_objs {
        let obj = RateSwap { id: crypt_obj.id, symbol: crypt_obj.symbol, rateUsd: crypt_obj.rateUsd.clone(), rateBtc: crypt_obj.rateUsd.parse::<f32>().unwrap() / &btcRate };
        data.push(obj);
    }
    data.sort_by(|a, b| a.rateBtc.partial_cmp(&b.rateBtc).unwrap().reverse());

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

おおお、割とやりたいことはできている気がする。
対btcではなく、コイン対コインでスワッピングする場合も同様のロジックで、交換する両方のコインのUSDレートで割ればスワッピングレートが計算できる。

Nice、次はaxumのログイン機能およびDB連携