#jwt #json #tokens #key-pair #web-api #jws

jwt-simple

为Rust提供的易于使用、安全、无偏见的JWT(JSON Web Tokens)实现

62个版本

0.12.9 2024年2月20日
0.12.7 2024年1月7日
0.12.6 2023年12月17日
0.12.1 2023年11月30日
0.2.2 2020年11月3日

#22 in Web编程

Download history 26606/week @ 2024-04-22 28174/week @ 2024-04-29 30060/week @ 2024-05-06 31327/week @ 2024-05-13 50437/week @ 2024-05-20 41198/week @ 2024-05-27 45631/week @ 2024-06-03 46512/week @ 2024-06-10 44938/week @ 2024-06-17 47897/week @ 2024-06-24 35019/week @ 2024-07-01 28248/week @ 2024-07-08 22473/week @ 2024-07-15 24352/week @ 2024-07-22 24543/week @ 2024-07-29 23449/week @ 2024-08-05

95,929 每月下载量
25 库中使用(直接使用 20 个)

ISC 许可证

175KB
4K SLoC

GitHub CI Docs.rs crates.io

JWT-Simple

为Rust提供的新JWT(JSON Web Tokens)实现,侧重于简单性,同时避免常见的JWT安全陷阱。

jwt-simple 无偏见,支持所有常见的认证和签名算法

JWT算法名称 描述
HS256 HMAC-SHA-256
HS384 HMAC-SHA-384
HS512 HMAC-SHA-512
BLAKE2B BLAKE2B-256
RS256 RSA with PKCS#1v1.5 padding / SHA-256
RS384 RSA with PKCS#1v1.5 padding / SHA-384
RS512 RSA with PKCS#1v1.5 padding / SHA-512
PS256 RSA with PSS padding / SHA-256
PS384 RSA with PSS padding / SHA-384
PS512 RSA with PSS padding / SHA-512
ES256 ECDSA over p256 / SHA-256
ES384 ECDSA over p384 / SHA-384
ES256K ECDSA over secp256k1 / SHA-256
EdDSA Ed25519

jwt-simple 可以直接编译为 WebAssembly/WASI。它与 Fastly 的 Compute 服务完全兼容。

重要提示:JWT 的目的是验证数据是由知道密钥的实体创建的。它不提供任何类型的机密性:JWT 数据简单地编码为 BASE64,并且未加密。

用法

cargo.toml:

[dependencies]
jwt-simple = "0.12"

Rust

use jwt_simple::prelude::*;

错误以 jwt_simple::Error 值的形式返回(即 thiserror crate 的 Error 类型的别称)。

认证(对称,HS* JWT算法)示例

身份验证方案使用相同的密钥来创建和验证令牌。换句话说,双方最终都需要相互信任,否则验证者也可以创建任意的令牌。

密钥和令牌创建

密钥创建

use jwt_simple::prelude::*;

// create a new key for the `HS256` JWT algorithm
let key = HS256Key::generate();

可以使用 key.to_bytes() 将密钥导出为字节,并使用 HS256Key::from_bytes() 进行恢复。

令牌创建

/// create claims valid for 2 hours
let claims = Claims::create(Duration::from_hours(2));
let token = key.authenticate(claims)?;

-> 完成!

令牌验证

let claims = key.verify_token::<NoCustomClaims>(&token, None)?;

-> 完成!无需额外步骤。

密钥过期时间、起始时间、身份验证标签等将自动验证。如果给定的密钥的身份验证标签无效,函数将失败,并返回 JWTError::InvalidAuthenticationTag

如果需要,可以在 claims 对象中检查完整的声明集。如果使用 NoCustomClaims,则表示应用程序仅使用标准声明集,但也可以支持应用程序定义的声明。

可以通过 ValidationOptions 结构启用可选的额外验证步骤。

let mut options = VerificationOptions::default();
// Accept tokens that will only be valid in the future
options.accept_future = true;
// Accept tokens even if they have expired up to 15 minutes after the deadline,
// and/or they will be valid within 15 minutes.
// Note that 15 minutes is the default, since it is very common for clocks to be slightly off.
options.time_tolerance = Some(Duration::from_mins(15));
// Reject tokens if they were issued more than 1 hour ago
options.max_validity = Some(Duration::from_hours(1));
// Reject tokens if they don't include an issuer from that set
options.allowed_issuers = Some(HashSet::from_strings(&["example app"]));

// see the documentation for the full list of available options

let claims = key.verify_token::<NoCustomClaims>(&token, Some(options))?;

请注意,allowed_issuersallowed_audiences 不是字符串,而是字符串的集合(使用 Rust 标准库中的 HashSet 类型),因为应用程序可以允许多个返回值。

签名(非对称,RS*PS*ES*EdDSA算法)示例

签名需要一个密钥对:用于创建令牌的密钥和只能验证它们的公钥。

如果双方最终不信任对方,则始终应使用签名方案,例如客户端和 API 提供者之间交换的令牌。

密钥对和令牌创建

密钥创建

ES256

use jwt_simple::prelude::*;

// create a new key pair for the `ES256` JWT algorithm
let key_pair = ES256KeyPair::generate();

// a public key can be extracted from a key pair:
let public_key = key_pair.public_key();

ES384

use jwt_simple::prelude::*;

// create a new key pair for the `ES384` JWT algorithm
let key_pair = ES384KeyPair::generate();

// a public key can be extracted from a key pair:
let public_key = key_pair.public_key();

可以将密钥导出为字节以供以后重用,并从字节导入,对于 RSA,可以单独从参数、DER 编码数据或 PEM 编码数据导入。

使用 OpenSSL 和 PEM 导入私钥创建 RSA 密钥对

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
let key_pair = RS384KeyPair::from_pem(private_pem_file_content)?;
let public_key = RS384PublicKey::from_pem(public_pem_file_content)?;

令牌创建和验证的方式与 HS* 算法相同,只是令牌使用密钥对创建,并使用相应的公钥进行验证。

令牌创建

/// create claims valid for 2 hours
let claims = Claims::create(Duration::from_hours(2));
let token = key_pair.sign(claims)?;

令牌验证

let claims = public_key.verify_token::<NoCustomClaims>(&token, None)?;

可用的验证选项与对称算法中使用的选项相同。

高级用法

自定义声明

默认情况下,声明对象支持所有标准声明,并且可以直接或通过方便的帮助器进行设置。

let claims = Claims::create(Duration::from_hours(2)).
    with_issuer("Example issuer").with_subject("Example subject");

但是,也可以定义应用程序定义的声明。这些只需在可序列化类型中存在即可(这需要 serde crate)。

#[derive(Serialize, Deserialize)]
struct MyAdditionalData {
   user_is_admin: bool,
   user_country: String,
}
let my_additional_data = MyAdditionalData {
   user_is_admin: false,
   user_country: "FR".to_string(),
};

使用自定义数据创建声明

let claims = Claims::with_custom_claims(my_additional_data, Duration::from_secs(30));

使用自定义数据验证声明。注意自定义数据类型的存在

let claims = public_key.verify_token::<MyAdditionalData>(&token, None)?;
let user_is_admin = claims.custom.user_is_admin;

在验证之前查看元数据

在标签或签名验证之前,属性(如密钥标识符)可能很有用,以便从一组中选择正确的密钥。

let metadata = Token::decode_metadata(&token)?;
let key_id = metadata.key_id();
let algorithm = metadata.algorithm();
// all other standard properties are also accessible

重要:密钥 ID 和算法都不值得信赖。这是 JWT 标准的一个无法修复的设计缺陷。

因此,algorithm 应仅用于调试目的,绝不能用于选择密钥类型。同样,key_id 应仅用于选择为同一算法制作的密钥集中的密钥。

如果最初使用签名方案创建了令牌,则至少必须禁止使用 HS* 进行验证。

创建和附加密钥标识符

密钥标识符指示验证者应使用哪个公钥(或共享密钥)进行验证。它们可以随时附加到现有的共享密钥、密钥对和公钥上。

let public_key_with_id = public_key.with_key_id(&"unique key identifier");

而不是将其委托给应用程序,jwt-simple 也可以为现有密钥创建这样的标识符。

let key_id = public_key.create_key_id();

这创建了一个文本编码的标识符,将其附加并返回。

如果标识符已附加到共享密钥或密钥对上,则使用它们创建的令牌将包括它。

减轻重放攻击的措施

jwt-simple 包含缓解重放攻击的机制。

  • 可以使用 create_nonce() 声明函数创建和附加到新令牌上的非ces。验证程序可以稍后拒绝任何不包括预期的非ces(required_nonce 验证选项)的令牌。
  • 验证程序可以拒绝创建时间过长的令牌,无论它们的过期日期是什么。这可以防止恶意(或受损)签发者使用的令牌时间过长。
  • 验证程序可以拒绝在某个日期之前创建的令牌。对于给定用户,可以将最后一次成功的认证日期存储在数据库中,并稍后与该选项一起使用以拒绝较旧的(重放的)令牌。

CWT(CBOR)支持

开发代码包括一个 cwt cargo功能,它启用对CWT令牌的实验性解析和验证。

请注意,CWT 不支持自定义声明。所需的标识符 尚未标准化

此外,现有的用于JSON和CBOR反序列化的Rust crate不安全。不受信任的方可以发送需要大量内存和CPU进行反序列化的序列化对象。已经为JSON添加了补丁,但使用当前的Rust工具,对于CBOR来说会很棘手。

作为一种缓解措施,我们强烈建议拒绝在应用程序上下文中会太大太多的令牌。可以通过 max_token_length 验证选项来完成此操作。

解决与boring库的编译问题

作为与依赖项(boring crate)的兼容性问题的临时解决方案,此库可以编译为仅使用Rust实现。

为此,请在您的Cargo配置中使用 default-features=false, features=["pure-rust"] 导入crate。

不要无条件的这样做。这只适用于非常特定的设置和目标,并且只在 boring crate 的问题得到解决之前。在Cargo中配置此设置的方式也可能在未来版本中发生变化。

针对 musl 库的静态构建不需要这种解决方案。只需使用 cargo-zigbuild 构建您的项目即可。

在Web浏览器中的使用

支持 wasm32-freestanding 目标(在Rust中有时仍称为 wasm32-unknown-unknown)。

然而,强烈建议使用原生JavaScript实现。JavaScript中有高质量的JWT实现,利用WebCrypto API,其性能和安全性比WebAssembly模块更优。

为什么还需要另一个JWT库

本包并非对JWT的支持。JWT 是一种糟糕的设计,是“但这是一种标准”并不一定意味着它是好的许多例子之一。

如果您控制令牌的创建和验证,我强烈推荐使用PASETOBiscuit

然而,JWT在业界仍被广泛使用,并且与流行的API通信仍然是绝对必要的。

本包的设计目标是

  • 易于使用,即使是对于初学者来说也很简单
  • 避免常见的JWT API陷阱
  • 支持广泛使用的功能。我本想将算法选择限制在Ed25519,但为了连接到现有的API,需要其他方法,因此只提供它们(除了明显的原因之外,不提供None签名方法)。
  • 最小化代码复杂性和外部依赖
  • 自动执行常见任务以防止误用。签名验证和声明验证会自动执行,而不是依赖于应用程序。
  • 仍然允许高级用户访问JWT令牌中包含的所有内容,如果他们确实需要的话
  • 在WebAssembly环境中直接工作,以便可以在函数即服务平台上使用。

依赖关系

~5–12MB
~250K SLoC