Open4
JWT
JWT
- 語源
- jsonwebtoken
構成
[ヘッダ].
[ペイロード].
[署名]
実際のjwt
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MDg4NDQyNzQsImV4cCI6MTcwOTQ0OTA3NCwic3ViIjoiYXV0aCIsInVzZXJfbmFtZSI6ImV4YW1wbGVfdXNlciJ9.NqM23O2483r-q6eJAMazwKngfWHinMY5_jod-BFQveI
作り方
ヘッダ
{
"typ": "JWT",
"alg": "HS256"
}
Base64エンコード
echo -n '{"alg":"HS256","typ":"JWT"}' | base64
結果
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
ペイロード
{
"iat": 1708844274,
"exp": 1709449074,
"sub": "auth",
"user_name": "example_user"
}
Base64エンコード
結果
eyJpYXQiOjE3MDg4NDQyNzQsImV4cCI6MTcwOTQ0OTA3NCwic3ViIjoiYXV0aCIsInVzZXJfbmFtZSI6ImV4YW1wbGVfdXNlciJ9
署名
ヘッダ
ペイロード
を繋げて、署名なしトークンを作成
署名なしTokenに対し、秘密鍵とHMAC-SHA256を用いて署名を生成
echo -n 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MDg4NDQyNzQsImV4cCI6MTcwOTQ0OTA3NCwic3ViIjoiYXV0aCIsInVzZXJfbmFtZSI6ImV4YW1wbGVfdXNlciJ9' | \
openssl dgst -binary -sha256 -hmac 'app-secret' | \
base64
生成された、トークンを全て [.] で繋げる
エンコード
前提として、name
password
をキーとする
let claims = ApiClaims::generate_claims(&User {
name: user.name.clone(),
password: user.password.clone(),
});
let token = ApiJwt::encode(claims);
ペイロードの部分を構築
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiClaims {
iat: i64, // issued at
exp: i64, // expiration
sub: String, // subject
user_name: String, // user_name
}
impl ClaimsGenerator<User> for ApiClaims {
fn generate_claims(user: &User) -> Self {
let now = chrono::Utc::now().timestamp();
let exp = now + 60 * 60 * 24 * 7; // 7 days
ApiClaims {
iat: now,
exp,
sub: "auth".to_string(),
user_name: user.name.clone(),
}
}
}
ヘッダーと署名の部分を含めてエンコードする
pub struct ApiJwt;
impl ApiJwt {
pub fn encode<T: Serialize>(claims: T) -> Result<String, JwtError> {
let header = Header {
typ: Some("JWT".into()),
alg: Algorithm::HS256,
..Default::default()
};
encode(
&header,
&claims,
&EncodingKey::from_secret(JWT_SECRET_KEY.as_ref()),
)
}
}
デコード
// decode token
let token = match ApiJwt::parse_header(&headers) {
Ok(token) => token,
Err(err) => {
eprintln!("Failed to parse token: {:?}", err);
return (StatusCode::UNAUTHORIZED, "Invalid token").into_response();
}
};
let token_data = match ApiJwt.decode(&token) {
Ok(token_data) => token_data,
Err(err) => {
eprintln!("Failed to decode token: {:?}", err);
return (StatusCode::UNAUTHORIZED, "Invalid token").into_response();
}
};
ヘッダーから取り出すtoken
を取り出す
fn parse_header(header: &HeaderMap) -> Result<String, CustomErr> {
match header.get(AUTHORIZATION) {
Some(token) => {
let mut split_token = token
.to_str()
.map_err(|_| CustomErr::InvalidHeader)?
.split_whitespace();
match split_token.next() {
Some(schema_type) if schema_type == "Bearer" => match split_token.next() {
Some(jwt_token) => Ok(jwt_token.to_string()),
None => Err(CustomErr::NoJwtTokenFound),
},
Some(_) | None => Err(CustomErr::InvalidSchemaType),
}
}
None => Err(CustomErr::NoAuthorizationHeader),
}
}
fn decode(&self, token: &str) -> Result<TokenData<ApiClaims>, CustomErr> {
decode::<ApiClaims>(
token,
&DecodingKey::from_secret(JWT_SECRET_KEY.as_ref()),
&Validation::default(),
)
.map_err(CustomErr::JwtError)
}
キーを使って、デコード
JWTにおけるログインの流れ
ログイン
- name=hoge, password=hogeとし、 /loginにPOSTリクエストする。
- APサーバは、DBに対し、ユーザの確認をリクエストする。
- DBは、APサーバに対し、対応するユーザIDを返す。
- APサーバは、ユーザIDと秘密鍵より、Tokenを生成する。
- APサーバは、ユーザにTokenを返す。
ユーザー情報取得
- AuthorizationにJWTを設定し、 /userにGETリクエストする。
- APサーバは、秘密鍵を用いてTokenを検証する。
- APサーバは、TokenからユーザIDを取得する。
- APサーバは、DBに対し、IDに対するユーザ情報をリクエストする。
- DBは、IDに対するユーザ情報を返す。
- APサーバは、ユーザにユーザ情報を返す。
この流れでは、ログイン時にjwtを取得して、Cookieなどに保存しておき、ユーザー取得時には、jwtを検証して、idを取り出し、そのidをキーにして、ユーザーを取得する