2023-2-05

rust

Posted by

applemango

今回はactix-webでjwtを用いてaccess tokenを使ったログイン機能を実装をする

準備

Cargo.toml

[package]
name = "jwt"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1.0", features = ["derive"] }
actix-web = "4"
actix-web-httpauth = "*"
jwt-simple = "0.10"
rust-crypto = "0.2"
derive_more = "0.99.17"

[dependencies.rusqlite]
version = "*"
features = ["bundled"]

[dependencies.uuid]
version = "1.2.2"
features = [
    "v4",
    "fast-rng",
    "macro-diagnostics",
] 

今回は下のプログラムに追加していく感じになります

use actix_web::{get, post, web, App, HttpServer, Responder, HttpResponse, HttpRequest};
use jwt_simple::prelude::*;
use serde::{Serialize, Deserialize};
use rusqlite::Connection;
use uuid::Uuid;

use crypto::sha2::Sha256;
use crypto::digest::Digest;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(
                web::scope("/token")
            )
            .service(
                web::scope("/user")
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

アクセストークンを発行する

今回はjwt_simpleを使います

まずはkeyを用意します、このkeyでトークンを作成し検証するのでこのkeyがばれると認証の意味をなさなくなるので気を付けましょう

今回は分かりやすいようb"secret"と言う弱いkeyを使っていますが実際に使う場合は複雑な物を使ってください

let token_key = HS256Key::from_bytes(b"secret");

次にトークンのデータを作成します、この状態ではまだトークンではありません

今回は有効期限を15分、ユーザーを1、idをuuidにしています

有効期限は短ければ短いほど良いです、長ければtokenを盗られた時のリスクが高くなります、15分なら最長15分は自由にアクセスされる可能性あります

let claims = Claims::create(Duration::from_mins(15))
    .with_subject(1)
    .with_jwt_id(Uuid::new_v4().to_string());

最後にkeyを使ってデータに署名を施して完成です

let access_token =  token_key.authenticate(claims).unwrap();

全体像はこんな感じになります

#[derive(Deserialize)]
struct CreateTokenRequest {
    username: String,
    password: String,
}


#[derive(Serialize)]
struct CreateTokenResponse {
    token: String,
}
#[post("/create")]
async fn create_token(data: web::Json<CreateTokenRequest>) -> impl Responder {
    if !(data.username == "apple" && data.password == "42") {
        return HttpResponse::BadRequest().body("user does not exist or password is wrong")
    }

    let token_key = HS256Key::from_bytes(b"secret");

    let claims = Claims::create(Duration::from_mins(15))
        .with_subject(1)
        .with_jwt_id(Uuid::new_v4().to_string());

    let access =  token_key.authenticate(claims).unwrap();

    HttpResponse::Ok().json(CreateTokenResponse {
        token: access,
    })
}

あとは適当に追加します

1

2

3

4

5

6

7

+

8

9

10

11

12

13

14

15

16

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(
                web::scope("/token")
                    .service(create_token)
            )
            .service(
                web::scope("/user")
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

試しに127.0.0.1:8080/token/createに以下のようなbodypostすると以下のように帰って来るはずです

body

{
  "username": "apple",
  "password": "42"
}

response

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NzU1NjQ3NjIsImV4cCI6MTY3NTU2NTY2MiwibmJmIjoxNjc1NTY0NzYyLCJzdWIiOiIxIiwianRpIjoiYTAzNzQ5YzAtNjE3Ny00NjY1LWFkNDMtYWFiNDNhMWJiZTE5In0.tw0UmKgU50eZrr6pdFc3qh6LAP6mKLh7G_dzDZ0Ko-M"
}

認証をする

認証も最初は作成と同じようにkeyを作成します

let token_key = HS256Key::from_bytes(b"secret");

次にkeyを使い検証します

失敗した場合はエラーが帰って来ますが今回は.unwrapを使って処理を打ち切っています

レスポンスなどを返さないのであまり良いとは言えないエラー処理ですが今回は認証がメインなので使っています

let claims = token_key.verify_token::<NoCustomClaims>(&get_token(req), None).unwrap();

これの全体像はこんな感じ

fn get_token(req: HttpRequest) -> String {
    return req.headers().get("Authorization").unwrap().to_str().unwrap()[7..].to_string();
}

#[get("/hello")]
async fn hello(req: HttpRequest) -> impl Responder {
    let token_key = HS256Key::from_bytes(b"secret");
    let claims = token_key.verify_token::<NoCustomClaims>(&get_token(req), None).unwrap();
    
    HttpResponse::Ok().body("Hello world!")
}

これも追加します

1

2

3

4

5

6

7

8

9

10

11

12

+

13

14

15

16

17

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(
                web::scope("/token")
                    .service(create_token)
            )
            .service(
                web::scope("/user")
            )
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

一応これで基本的な使い方は終わりです

一応そのあとも、リフレッシュトークンやdb連携、ログアウト方法などが続きます

多言語のフレームワークを紹介した時はこのまま応用も続けて思った5倍は長くなったので今回は分割します

やっぱり後少し続けます

リフレッシュトークンを使う

先ほどアクセストークンを作成しましたが有効期限が15分と不便なのでリフレッシュトークンを作成しましょう

リフレッシュトークンはアクセストークンを作成する為のトークンです

アクセストークンを作成するためにしか使わないので比較的安全に長い時間保持できます

まず/token/でリフレッシュトークンも一緒に作成するようにしましょう

またリフレッシュトークンとアクセストークンを区別するためClaims::createじゃなくClaims::with_custom_claimsを使いrefreshと言うbool値を入れています

refreshと期限以外は同じです

1

2

3

4

5

6

7

8

9

10

11

+

12

13

14

+

15

+

16

+

17

+

18

19

20

21

22

23

24

25

26

27

-

28

+

29

30

31

-

32

33

34

+

35

+

36

+

37

+

38

39

40

41

+

42

43

#[derive(Deserialize)]
struct CreateTokenRequest {
    username: String,
    password: String,
}


#[derive(Serialize)]
struct CreateTokenResponse {
    token: String,
    refresh_token: String,
}

#[derive(Serialize, Deserialize)]
struct TokenClaims {
    refresh: bool,
}

#[post("/create")]
async fn create_token(data: web::Json<CreateTokenRequest>) -> impl Responder {
    if !(data.username == "apple" && data.password == "42") {
        return HttpResponse::BadRequest().body("user does not exist or password is wrong")
    }

    let token_key = HS256Key::from_bytes(b"secret");

    let claims = Claims::create(Duration::from_mins(15))
    let claims = Claims::with_custom_claims(TokenClaims {refresh: false}, Duration::from_mins(15))
        .with_subject(1)
        .with_jwt_id(Uuid::new_v4().to_string());

    let access =  token_key.authenticate(claims).unwrap();

    let claims = Claims::with_custom_claims(TokenClaims {refresh: true}, Duration::from_hours(24))
        .with_subject(1)
        .with_jwt_id(Uuid::new_v4().to_string());
    let refresh =  token_key.authenticate(claims).unwrap();

    HttpResponse::Ok().json(CreateTokenResponse {
        token: access,
        refresh_token: refresh,
    })
}

また認証も少し変えなければなりません

1

2

3

4

5

6

7

8

-

9

+

10

+

11

+

12

+

13

14

15

fn get_token(req: HttpRequest) -> String {
    return req.headers().get("Authorization").unwrap().to_str().unwrap()[7..].to_string();
}

#[get("/hello")]
async fn hello(req: HttpRequest) -> impl Responder {
    let token_key = HS256Key::from_bytes(b"secret");
    let claims = token_key.verify_token::<NoCustomClaims>(&get_token(req), None).unwrap();
    let claims = token_key.verify_token::<TokenClaims>(&get_token(req), None).unwrap();
    if claims.custom.refresh {
        return HttpResponse::BadRequest().body("Refresh tokens are not allowed")
    }

    HttpResponse::Ok().body("Hello world!")
}

トークンをリフレッシュする

それではいよいよトークンをリフレッシュ出来るようにしましょう

と言ってもリフレッシュトークンの認証に成功したらアクセストークンを作成するだけですが

#[derive(Serialize)]
struct CreateRefreshTokenResponse {
    token: String,
}
#[post("/refresh")]
async fn refresh_token(req: HttpRequest) -> impl Responder {
    let token_key = HS256Key::from_bytes(b"secret");
    let claims = token_key.verify_token::<TokenClaims>(&get_token(req), None).unwrap();
    if ! claims.custom.refresh {
        return HttpResponse::BadRequest().body("Access tokens are not allowed")
    }

    let claims = Claims::with_custom_claims(TokenClaims {refresh: false}, Duration::from_mins(15))
        .with_subject(1)
        .with_jwt_id(Uuid::new_v4().to_string());
    let token =  token_key.authenticate(claims).unwrap();
    HttpResponse::Ok().json(CreateRefreshTokenResponse {
        token: token
    })
}

最終的なコード

これまでのコードをまとめたものです

use actix_web::{get, post, web, App, HttpServer, Responder, HttpResponse, HttpRequest};
use jwt_simple::prelude::*;
use serde::{Serialize, Deserialize};
use rusqlite::Connection;
use uuid::Uuid;

use crypto::sha2::Sha256;
use crypto::digest::Digest;

#[derive(Deserialize)]
struct CreateTokenRequest {
    username: String,
    password: String,
}

#[derive(Serialize)]
struct CreateTokenResponse {
    token: String,
    refresh_token: String,
}

#[derive(Serialize)]
struct CreateRefreshTokenResponse {
    token: String,
}

#[derive(Serialize, Deserialize)]
struct TokenClaims {
    refresh: bool,
}

#[post("/")]
async fn create_token(data: web::Json<CreateTokenRequest>) -> impl Responder {
    if !(data.username == "apple" && data.password == "42") {
        return HttpResponse::BadRequest().body("user does not exist or password is wrong")
    }

    let token_key = HS256Key::from_bytes(b"secret");

    let claims = Claims::with_custom_claims(TokenClaims {refresh: false}, Duration::from_mins(15))
        .with_subject(1)
        .with_jwt_id(Uuid::new_v4().to_string());
    let access =  token_key.authenticate(claims).unwrap();
    
    let claims = Claims::with_custom_claims(TokenClaims {refresh: true}, Duration::from_hours(24))
        .with_subject(1)
        .with_jwt_id(Uuid::new_v4().to_string());
    let refresh =  token_key.authenticate(claims).unwrap();

    HttpResponse::Ok().json(CreateTokenResponse {
        token: access,
        refresh_token: refresh,
    })
}

#[post("/refresh")]
async fn refresh_token(req: HttpRequest) -> impl Responder {
    let token_key = HS256Key::from_bytes(b"secret");
    let claims = token_key.verify_token::<TokenClaims>(&get_token(req), None).unwrap();
    if ! claims.custom.refresh {
        return HttpResponse::BadRequest().body("Refresh tokens are not allowed")
    }

    let claims = Claims::with_custom_claims(TokenClaims {refresh: false}, Duration::from_mins(15))
        .with_subject(1)
        .with_jwt_id(Uuid::new_v4().to_string());
    let token =  token_key.authenticate(claims).unwrap();
    HttpResponse::Ok().json(CreateRefreshTokenResponse {
        token: token
    })
}

fn get_token(req: HttpRequest) -> String {
    return req.headers().get("Authorization").unwrap().to_str().unwrap()[7..].to_string();
}

#[get("/hello")]
async fn hello(req: HttpRequest) -> impl Responder {
    let token_key = HS256Key::from_bytes(b"secret");
    let claims = token_key.verify_token::<TokenClaims>(&get_token(req), None).unwrap();
    if claims.custom.refresh {
        return HttpResponse::BadRequest().body("Refresh tokens are not allowed")
    }
    HttpResponse::Ok().body("Hello world!")
}


#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(
                web::scope("/token")
                    .service(create_token ) 
                    .service(refresh_token)
            )
            .service(
                web::scope("/user")

            )
            .service(hello)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

続く

このドキュメントどう?

emoji
emoji
emoji
emoji