実用的なさびウェブ開発‐認証


シリーズのこのポストでは、私は認証をカバーしています.これはこれに基づいていますone しかし、私はActixウェブの1.0のバージョンを使用します.
私たちは使用するつもりですjwt クッキーでユーザーを認証するために、1つのセキュリティ考慮はクッキーを使用するとき、CSRF脆弱性です.ローカルストレージを使用する場合、XSS保護が必要になります.JWTを使用するときには、他のセキュリティ上の注意事項がありますthese . 他のセキュリティ提案がある場合は、コメントをしてください.
登録後にユーザーを作成するでしょう、セキュリティを向上させるには、電子メールの検証、captchaまたは2 faを追加することができます.
いくつかの箱を追加する必要があります.src/Cargo.toml :
jsonwebtoken = "6"
bcrypt = "0.4.0"
chrono = { version = "0.4.6", features = ["serde"] }
csrf-token = { git = "ssh://[email protected]/3dom-co-jp/csrf-token.git", branch="v0.2.x" }
ユーザーモデルも必要ですが、まずテーブルの移行を作成しましょう.
diesel migration generate create_users
生成された移行:migrations/2019-05-19-165021_create_users/up.sql :
CREATE TABLE users (
  email VARCHAR(100) NOT NULL PRIMARY KEY,
  password VARCHAR(64) NOT NULL,
  created_at TIMESTAMP NOT NULL
);
CREATE INDEX users_email_company_idx ON users (email, company);
migrations/2019-05-19-165021_create_users/down.sql :
DROP TABLE users;
diesel migration run

ユーザモデル
ユーザーモデルは、次のように使用されるユーザーを作成するにはRegisterUser 構造体を通して作成し、NewUser , 私はこのようにしていますpassword_confirmation データベース内のフィールドですが、register actionで必要です.他の構造体はAuthUser , 私は認証のためにそれを使用しています.email and password , 少しのboilerplateがそれの価値がある報酬のようです.src/models/mod.rs :
pub mod user;
src/models/user.rs :
use chrono::NaiveDateTime; // This type is used for date field in Diesel.
use crate::schema::users;

#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
#[table_name = "users"]
pub struct User {
    #[serde(skip)] // we're removing id from being show in the response
    pub id: i32,
    pub email: String,
    pub company: String,
    #[serde(skip)] // we're removing password from being show in the response
    pub password: String,
    pub created_at: NaiveDateTime
}

#[derive(Debug, Serialize, Deserialize, Insertable)]
#[table_name = "users"]
pub struct NewUser {
    pub email: String,
    pub company: String,
    pub password: String,
    pub created_at: NaiveDateTime
}

use bcrypt::{hash, DEFAULT_COST};
use diesel::PgConnection;
use chrono::Local;
use crate::errors::MyStoreError;

// MyStoreError is a custom error that I will show it next.
impl User {
    pub fn create(register_user: RegisterUser, connection: &PgConnection) ->
     Result<User, MyStoreError> {
        use diesel::RunQueryDsl;

        Ok(diesel::insert_into(users::table)
            .values(NewUser {
                email: register_user.email,
                company: register_user.company,
                password: Self::hash_password(register_user.password)?,
                created_at: Local::now().naive_local()
            })
            .get_result(connection)?)
    }

    // This might look kind of weird, 
    // but if something fails it would chain 
    // to our MyStoreError Error, 
    // otherwise it will gives us the hash, 
    // we still need to return a result 
    // so we wrap it in an Ok variant from the Result type. 
    pub fn hash_password(plain: String) -> Result<String, MyStoreError> {
        Ok(hash(plain, DEFAULT_COST)?)
    }
}

#[derive(Deserialize)]
pub struct RegisterUser {
    pub email: String,
    pub company: String,
    pub password: String,
    pub password_confirmation: String
}

impl RegisterUser {
    pub fn validates(self) ->
     Result<RegisterUser, MyStoreError> {
         if self.password == self.password_confirmation {
             Ok(self)
         } else {
             Err(
                 MyStoreError::PasswordNotMatch(
                     "Password and Password Confirmation does not match".to_string()
                 )
             )
         }
    }
}

#[derive(Deserialize)]
pub struct AuthUser {
    pub email: String,
    pub password: String
}

impl AuthUser {

    // The good thing about ? syntax and have a custom error is 
    // that the code would look very straightforward, I mean, 
    // the other way would imply a lot of pattern matching 
    // making it look ugly. 
    pub fn login(&self, connection: &PgConnection) ->
     Result<User, MyStoreError> {
        use bcrypt::verify;
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use diesel::ExpressionMethods;
        use crate::schema::users::dsl::email;

        let mut records =
            users::table
                .filter(email.eq(&self.email))
                .load::<User>(connection)?;

        let user =
            records
                .pop()
                .ok_or(MyStoreError::DBError(diesel::result::Error::NotFound))?;

        let verify_password =
            verify(&self.password, &user.password)
                .map_err( |_error| {
                    MyStoreError::WrongPassword(
                        "Wrong password, check again please".to_string()
                    )
                })?;

        if verify_password {
            Ok(user)
        } else {
            Err(MyStoreError::WrongPassword(
                "Wrong password, check again please".to_string()
            ))
        }

    }
}
あなたが走るならばcargo build エラーが表示されます.
the trait `diesel::Expression` is not implemented for 
`chrono::naive::datetime::NaiveDateTime`
それは、我々がちょうど2010年にディーゼルに機能を加える必要があることを示しますCargo.toml , 次のようになります.src/Cargo.toml :
diesel = { version = "1.0.0", features = ["postgres", "r2d2", "chrono"] }
さて、問題なくコンパイルする必要があります.

カスタムエラー
ユーザーモデルでは、多くのMyStoreError エラーは、アイデアを操作することができますし、簡単に読みやすいコードを持っている統一カスタムエラーを持っている? 構文のシュガーは、結果の型を返す別の関数を呼び出す他の関数にチェックインすることができるので、同じエラーが発生する必要があるので、Rust bookは ? operator .src/errors.rs :
use std::fmt;
use bcrypt::BcryptError;
use diesel::result;

pub enum MyStoreError {
    HashError(BcryptError),
    DBError(result::Error),
    PasswordNotMatch(String),
    WrongPassword(String)
}

// We need this to performs a conversion from BcryptError to MyStoreError
impl From<BcryptError> for MyStoreError {
    fn from(error: BcryptError) -> Self {
        MyStoreError::HashError(error)
    }
}

// We need this to performs a conversion from diesel::result::Error to MyStoreError
impl From<result::Error> for MyStoreError {
    fn from(error: result::Error) -> Self {
        MyStoreError::DBError(error)
    }
}

// We need this so we can use the method to_string over MyStoreError 
impl fmt::Display for MyStoreError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyStoreError::HashError(error) => write!(f, "{}", error),
            MyStoreError::DBError(error) => write!(f, "{}", error),
            MyStoreError::PasswordNotMatch(error) => write!(f, "{}", error),
            MyStoreError::WrongPassword(error) => write!(f, "{}", error)
        }
    }
}
src/main.rs :
pub mod errors;

ハンドラ
今、私たちはちょうどユーザを登録するためのハンドラとログインのためにハンドラを必要とします.src/handlers/register.rs :
use actix_web::web;
use crate::db_connection::PgPool;
use actix_web::HttpResponse;
use crate::handlers::pg_pool_handler;

use crate::models::user::{ User, RegisterUser };

// We get a new connection pool, validates the data, 
// `password` and `password_confirmation` should be the same, 
// finally we create the user and return it.
pub fn register(new_user: web::Json<RegisterUser>, pool: web::Data<PgPool>) ->
 Result<HttpResponse, HttpResponse> {
    let pg_pool = pg_pool_handler(pool)?;
    let register_user = new_user
        .into_inner()
        .validates()
        .map_err(|e| {
           HttpResponse::InternalServerError().json(e.to_string())
        })?;
    User::create(register_user, &pg_pool)
        .map(|user| HttpResponse::Ok().json(user))
        .map_err(|e| {
           HttpResponse::InternalServerError().json(e.to_string())
        })
}

src/handlers/authentication.rs :
use actix_web::HttpResponse;
use actix_web::middleware::identity::Identity;
use actix_web::web;
use csrf_token::CsrfTokenGenerator;
use hex;
use crate::utils::jwt::create_token;

use crate::models::user::AuthUser;
use crate::db_connection::PgPool;
use crate::handlers::pg_pool_handler;

// We get a new connection pool, then look up for the user,
// If there is no user a NotFound error would raise otherwise
// this would just through an InternalServerError.
pub fn login(auth_user: web::Json<AuthUser>, 
             id: Identity, 
             pool: web::Data<PgPool>, 
             generator: web::Data<CsrfTokenGenerator>) 
    -> Result<HttpResponse, HttpResponse> {
    let pg_pool = pg_pool_handler(pool)?;
    let user = auth_user
        .login(&pg_pool)
        .map_err(|e| {
            match e {
                MyStoreError::DBError(diesel::result::Error::NotFound) =>
                    HttpResponse::NotFound().json(e.to_string()),
                _ =>
                    HttpResponse::InternalServerError().json(e.to_string())
            }
        })?;

    // This is the jwt token we will send in a cookie.
    let token = create_token(&user.email, &user.company)?;

    id.remember(token);

    // Finally our response will have a csrf token for security. 
    let response =
        HttpResponse::Ok()
        .header("X-CSRF-TOKEN", hex::encode(generator.generate()))
        .json(user);
    Ok(response)
}

pub fn logout(id: Identity) -> Result<HttpResponse, HttpResponse> {
    id.forget();
    Ok(HttpResponse::Ok().into())
}
src/handlers/mod.rs :
pub mod products;
pub mod register;
pub mod authentication;

use actix_web::web;
use actix_web::HttpResponse;
use crate::db_connection::{ PgPool, PgPooledConnection };

// Because I'm using this function a lot, 
// I'm including it in the mod file accessible to all handlers.
pub fn pg_pool_handler(pool: web::Data<PgPool>) -> Result<PgPooledConnection, HttpResponse> {
    pool
    .get()
    .map_err(|e| {
        HttpResponse::InternalServerError().json(e.to_string())
    })
}

Webトークン実装
ここでJWTライブラリを利用できるようになりました.utilsというフォルダを作成し、ファイル名を作成しましょうjwt.rs .src/utils/jwt.rs :
use jwt::{decode, encode, Header, Validation};
use chrono::{Local, Duration};
use actix_web::HttpResponse;

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    company: String,
    exp: usize
}

// We're using a struct so we can implement a conversion from
// Claims to SlimUser, useful in the decode function.
pub struct SlimUser {
    pub email: String,
    pub company: String
}

impl From<Claims> for SlimUser {
    fn from(claims: Claims) -> Self {
        SlimUser {
            email: claims.sub,
            company: claims.company
        }
    }
}

impl Claims {
    fn with_email(email: &str, company: &str) -> Self {
        Claims {
            sub: email.into(),
            company: company.into(),
            exp: (Local::now() + Duration::hours(24)).timestamp() as usize
        }
    }
}

pub fn create_token(email: &str, company: &str) -> Result<String, HttpResponse> {
    let claims = Claims::with_email(email, company);
    encode(&Header::default(), &claims, get_secret())
        .map_err(|e| HttpResponse::InternalServerError().json(e.to_string()))
}

pub fn decode_token(token: &str) -> Result<SlimUser, HttpResponse> {
    decode::<Claims>(token, get_secret(), &Validation::default())
        .map(|data| data.claims.into())
        .map_err(|e| HttpResponse::Unauthorized().json(e.to_string()))
}

fn get_secret<'a>() -> &'a [u8] {
    dotenv!("JWT_SECRET").as_bytes()
}

src/utils/mod.rs :
pub mod jwt;

フロムリクエスト
私たちの実装ではActivx Web FromRequest Traitを使用する必要があります.これは、認証でログを使用することができます.このアイデアは、すべてのリクエストをキャッチし、CSRFトークンとJWTトークンを検証するためにこの特性を使用します.
製品のハンドラではいくつかの変更があります.なぜなら、リクエストにloggeduser構造体が必要だからです.この投稿でコードを省略しますが、Githubのソースコードを見ることができます.src/handlers/mod.rs :
use actix_web::{ FromRequest, HttpRequest, dev };
use actix_web::middleware::identity::Identity;
use crate::utils::jwt::{ decode_token, SlimUser };
pub type LoggedUser = SlimUser;

use hex;
use csrf_token::CsrfTokenGenerator;

impl FromRequest for LoggedUser {
    type Error = HttpResponse;
    type Config = ();
    type Future = Result<Self, HttpResponse>;

    fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
        let generator = 
            req.app_data::<CsrfTokenGenerator>()
            .ok_or(HttpResponse::InternalServerError())?;

        let csrf_token =
            req
                .headers()
                .get("x-csrf-token")
                .ok_or(HttpResponse::Unauthorized())?;

        let decoded_token =
            hex::decode(&csrf_token)
                .map_err(|error| HttpResponse::InternalServerError().json(error.to_string()))?;

        generator
            .verify(&decoded_token)
            .map_err(|_| HttpResponse::Unauthorized())?;

        // We're using the CookieIdentityPolicy middleware
        // to handle cookies, with this implementation this 
        // will validate the cookie according to the secret
        // provided in main function
        if let Some(identity) = Identity::from_request(req, payload)?.identity() {
            let user: SlimUser = decode_token(&identity)?;
            return Ok(user as LoggedUser);
        }  
        Err(HttpResponse::Unauthorized().into())
    }
}

最後にmain.rs ファイルはこのように見えます、我々は異なるミドルウェア、ログのための1、Coosのためのもう一つとCSRFトークンのためにもう一つを使います、しかし、最後に我々が我々が我々が共有しているアプリケーションデータであることを意味するデータメソッドを使用する最後のものではありませんが、データベース接続のようなアプリケーションですsrc/main.rs :
fn main() {
    std::env::set_var("RUST_LOG", "actix_web=debug");
    env_logger::init();
    let sys = actix::System::new("mystore");

    let csrf_token_header = header::HeaderName::from_lowercase(b"x-csrf-token").unwrap();

    HttpServer::new(
    move || App::new()
        .wrap(Logger::default())
        // we implement middleares with the warp method
        .wrap( 
            IdentityService::new(
                CookieIdentityPolicy::new(dotenv!("SECRET_KEY").as_bytes())
                    .domain(dotenv!("MYSTOREDOMAIN"))
                    .name("mystorejwt")
                    .path("/")
                    .max_age(Duration::days(1).num_seconds())
                    .secure(dotenv!("COOKIE_SECURE").parse().unwrap())
            )
        )
        .wrap(
            cors::Cors::new()
                .allowed_origin(dotenv!("ALLOWED_ORIGIN"))
                .allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
                .allowed_headers(vec![header::AUTHORIZATION,
                                      header::CONTENT_TYPE,
                                      header::ACCEPT,
                                      csrf_token_header.clone()])
                .expose_headers(vec![csrf_token_header.clone()])
                .max_age(3600)
        )
        .data(
            CsrfTokenGenerator::new(
                dotenv!("CSRF_TOKEN_KEY").as_bytes().to_vec(),
                Duration::hours(1)
            )
        )
        .data(establish_connection())
        .service(
            web::resource("/products")
                .route(web::get().to(handlers::products::index))
                .route(web::post().to(handlers::products::create))
        )
        .service(
            web::resource("/products/{id}")
                .route(web::get().to(handlers::products::show))
                .route(web::delete().to(handlers::products::destroy))
                .route(web::patch().to(handlers::products::update))
        )
        .service(
            web::resource("/register")
                .route(web::post().to(handlers::register::register))
        )
        .service(
            web::resource("/auth")
                .route(web::post().to(handlers::authentication::login))
                .route(web::delete().to(handlers::authentication::logout))
        )
    )
    .bind("127.0.0.1:8088").unwrap()
    .start();

    println!("Started http server: 127.0.0.1:8088");
    let _ = sys.run();
}

結論
我々のユーザーを認証するとき、我々は慎重である必要があります、あなたが取るかもしれないいくつかのセキュリティ注意があります、OWASPは我々のウェブアプリケーションを保護するいくつかの資源を提供しますtop ten , だから、ちょうどあなたの研究を行うと、あなたは、アプリケーションを保護するために別の提案がある場合は、コメントをしてください.
完全なソースコードを見ることができますhere