#jwt #jwks #jwk #set-key #gcp

jwks-client

使用 JSON Web 密钥集(JWKS)验证 JWT 令牌的库

8 个版本

0.2.0 2020 年 1 月 13 日
0.1.8 2019 年 12 月 20 日
0.1.6 2019 年 9 月 29 日

#8 in #set-key

Download history 83/week @ 2024-03-11 67/week @ 2024-03-18 20/week @ 2024-03-25 85/week @ 2024-04-01 26/week @ 2024-04-08 20/week @ 2024-04-15 79/week @ 2024-04-22 81/week @ 2024-04-29 52/week @ 2024-05-06 38/week @ 2024-05-13 85/week @ 2024-05-20 141/week @ 2024-05-27 79/week @ 2024-06-03 98/week @ 2024-06-10 88/week @ 2024-06-17 72/week @ 2024-06-24

每月 339 次下载
用于 2 crates

MIT 许可证

43KB
726

Build Status License:MIT License:Apache Minimum rustc version

JWKS-Client 是一个用 Rust 编写的库,用于使用 JSON Web 密钥存储解码和验证 JWT 令牌。

重大更改

现在版本为 2.0。由于 Genna Wingert 的贡献,支持异步/等待。需要 Rust 稳定版 1.39 或更高版本

** 重要 **

JWKS-Client 被设计为与使用 Rocket 的项目一起工作。遗憾的是,在 crates.io 上的 Rocket 版本与 JWKS-Client 所需的 Ring 版本不兼容。在下一个版本发布之前,请考虑在您的 Cargo.toml 中使用以下内容

[dependencies]
jwks-client = "0.1.4"
rocket = { git = "https://github.com/jfbilodeau/Rocket", version = "0.5.0-dev"}
# Other dependencies...

[dependencies.rocket_contrib]
version = "0.5.0-dev"
git = "https://github.com/jfbilodeau/Rocket"
# Other options...

功能

库范围

  • 无 panic!
  • 使用 Rust 稳定版(1.40)构建
  • 为生产系统设计(不是学术项目)
  • 结果简洁(例如,请参阅 error::Type

JWKS 密钥存储

  • 从 HTTP 地址下载密钥集
  • 将 JWT 令牌解码为头部、有效负载和签名
  • 验证令牌签名、过期时间和不可用时间
  • 确定何时刷新密钥

JWT

  • 在用户定义的结构体中传输头部和有效负载。请参阅下面的示例[^1]
  • 标准头部和有效负载字段的访问器

JWKS-Client 是专门为了解码 GCP/Firebase JWT 而创建的,但应该可以稍微修改后使用。请与我联系以提出对不同的 JWKS 密钥存储的支持。欢迎反馈、建议、投诉和批评。

基本用法

以下演示了如何从 HTTP 地址加载一组密钥并使用这些密钥验证 JWT 令牌

use jwks_client::error::Error;
use jwks_client::keyset::KeyStore;

#[tokio::main]
async fn main() {
    let jkws_url = "https://raw.githubusercontent.com/jfbilodeau/jwks-client/0.1.8/test/test-jwks.json";

    let key_set = KeyStore::new_from(jkws_url).await.unwrap();

    // ...

    let token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ.eyJuYW1lIjoiQWRhIExvdmVsYWNlIiwiaXNzIjoiaHR0cHM6Ly9jaHJvbm9nZWFycy5jb20vdGVzdCIsImF1ZCI6InRlc3QiLCJhdXRoX3RpbWUiOjEwMCwidXNlcl9pZCI6InVpZDEyMyIsInN1YiI6InNidTEyMyIsImlhdCI6MjAwLCJleHAiOjUwMCwibmJmIjozMDAsImVtYWlsIjoiYWxvdmVsYWNlQGNocm9ub2dlYXJzLmNvbSJ9.eTQnwXrri_uY55fS4IygseBzzbosDM1hP153EZXzNlLH5s29kdlGt2mL_KIjYmQa8hmptt9RwKJHBtw6l4KFHvIcuif86Ix-iI2fCpqNnKyGZfgERV51NXk1THkgWj0GQB6X5cvOoFIdHa9XvgPl_rVmzXSUYDgkhd2t01FOjQeeT6OL2d9KdlQHJqAsvvKVc3wnaYYoSqv2z0IluvK93Tk1dUBU2yWXH34nX3GAVGvIoFoNRiiFfZwFlnz78G0b2fQV7B5g5F8XlNRdD1xmVZXU8X2-xh9LqRpnEakdhecciFHg0u6AyC4c00rlo_HBb69wlXajQ3R4y26Kpxn7HA";

    match key_set.verify(token) {
        Ok(jwt) => {
            println!("name={}", jwt.payload().get_str("name").unwrap());
        }
        Err(Error { msg, typ: _ }) => {
            eprintln!("Could not verify token. Reason: {}", msg);
        }
    }
}

JWKS-Client 可以用来简单地解码 JWT 令牌而不验证签名。

use jwks_client::keyset::KeyStore;

fn main() {
    let key_store = KeyStore::new();

    let token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ.eyJuYW1lIjoiQWRhIExvdmVsYWNlIiwiaXNzIjoiaHR0cHM6Ly9jaHJvbm9nZWFycy5jb20vdGVzdCIsImF1ZCI6InRlc3QiLCJhdXRoX3RpbWUiOjEwMCwidXNlcl9pZCI6InVpZDEyMyIsInN1YiI6InNidTEyMyIsImlhdCI6MjAwLCJleHAiOjUwMCwibmJmIjozMDAsImVtYWlsIjoiYWxvdmVsYWNlQGNocm9ub2dlYXJzLmNvbSJ9.eTQnwXrri_uY55fS4IygseBzzbosDM1hP153EZXzNlLH5s29kdlGt2mL_KIjYmQa8hmptt9RwKJHBtw6l4KFHvIcuif86Ix-iI2fCpqNnKyGZfgERV51NXk1THkgWj0GQB6X5cvOoFIdHa9XvgPl_rVmzXSUYDgkhd2t01FOjQeeT6OL2d9KdlQHJqAsvvKVc3wnaYYoSqv2z0IluvK93Tk1dUBU2yWXH34nX3GAVGvIoFoNRiiFfZwFlnz78G0b2fQV7B5g5F8XlNRdD1xmVZXU8X2-xh9LqRpnEakdhecciFHg0u6AyC4c00rlo_HBb69wlXajQ3R4y26Kpxn7HA";

    let jwt = key_store.decode(token).unwrap();

    if jwt.expired().unwrap_or(false) {
        println!("Sorry, token expired")
    } else {
        let result = jwt.payload().get_str("name");

        match result {
            Some(name) => {
                println!("Welcome, {}!", name);
            }
            None => {
                println!("Welcome, anonymous");
            }
        }
    }
}

JWKS-Client 提供描述性的错误结果

use jwks_client::error::{Error, Type};
use jwks_client::keyset::KeyStore;

#[rustfmt::skip]
#[tokio::main]
async fn main() {
    let url = "https://raw.githubusercontent.com/jfbilodeau/jwks-client/0.1.8/test/test-jwks.json";
    let token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ.eyJuYW1lIjoiQWRhIExvdmVsYWNlIiwiaXNzIjoiaHR0cHM6Ly9jaHJvbm9nZWFycy5jb20vdGVzdCIsImF1ZCI6InRlc3QiLCJhdXRoX3RpbWUiOjEwMCwidXNlcl9pZCI6InVpZDEyMyIsInN1YiI6InNidTEyMyIsImlhdCI6MjAwLCJleHAiOjUwMCwibmJmIjozMDAsImVtYWlsIjoiYWxvdmVsYWNlQGNocm9ub2dlYXJzLmNvbSJ9.eTQnwXrri_uY55fS4IygseBzzbosDM1hP153EZXzNlLH5s29kdlGt2mL_KIjYmQa8hmptt9RwKJHBtw6l4KFHvIcuif86Ix-iI2fCpqNnKyGZfgERV51NXk1THkgWj0GQB6X5cvOoFIdHa9XvgPl_rVmzXSUYDgkhd2t01FOjQeeT6OL2d9KdlQHJqAsvvKVc3wnaYYoSqv2z0IluvK93Tk1dUBU2yWXH34nX3GAVGvIoFoNRiiFfZwFlnz78G0b2fQV7B5g5F8XlNRdD1xmVZXU8X2-xh9LqRpnEakdhecciFHg0u6AyC4c00rlo_HBb69wlXajQ3R4y26Kpxn7HA";

    let key_set = KeyStore::new_from(url).await.unwrap();

    match key_set.verify(token) {
        Ok(jwt) => {
            println!("name={}", jwt.payload().get_str("name").unwrap());
        }
        Err(Error {
            msg,
            typ: Type::Header,
        }) => {
            eprintln!("Problem with header. Message: {}", msg);
        }
        Err(Error {
            msg,
            typ: Type::Payload,
        }) => {
            eprintln!("Problem with payload. Message: {}", msg);
        }
        Err(Error {
            msg,
            typ: Type::Signature,
        }) => {
            eprintln!("Problem with signature. Message: {}", msg);
        }
        Err(Error {
            msg: _,
            typ: Type::Expired,
        }) => {
            eprintln!("Token is expired.");
        }
        Err(Error {
            msg: _,
            typ: Type::Early,
        }) => {
            eprintln!("Too early to use token.");
        }
        Err(e) => {
            eprintln!("Something else went wrong. Message {:?}", e);
        }
    }
}

[^1] JWKS-Client 可以将 JWT 有效负载(声明)解码到结构体中

use serde_derive::Deserialize;

use jwks_client::keyset::KeyStore;

fn main() {
    #[derive(Deserialize)]
    pub struct MyClaims {
        pub iss: String,
        pub name: String,
        pub email: String,
    }

    let url = "https://raw.githubusercontent.com/jfbilodeau/jwks-client/0.1.8/test/test-jwks.json";

    let key_store = KeyStore::new_from(url).unwrap();

    let token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ.eyJuYW1lIjoiQWRhIExvdmVsYWNlIiwiaXNzIjoiaHR0cHM6Ly9jaHJvbm9nZWFycy5jb20vdGVzdCIsImF1ZCI6InRlc3QiLCJhdXRoX3RpbWUiOjEwMCwidXNlcl9pZCI6InVpZDEyMyIsInN1YiI6InNidTEyMyIsImlhdCI6MjAwLCJleHAiOjUwMCwibmJmIjozMDAsImVtYWlsIjoiYWxvdmVsYWNlQGNocm9ub2dlYXJzLmNvbSJ9.eTQnwXrri_uY55fS4IygseBzzbosDM1hP153EZXzNlLH5s29kdlGt2mL_KIjYmQa8hmptt9RwKJHBtw6l4KFHvIcuif86Ix-iI2fCpqNnKyGZfgERV51NXk1THkgWj0GQB6X5cvOoFIdHa9XvgPl_rVmzXSUYDgkhd2t01FOjQeeT6OL2d9KdlQHJqAsvvKVc3wnaYYoSqv2z0IluvK93Tk1dUBU2yWXH34nX3GAVGvIoFoNRiiFfZwFlnz78G0b2fQV7B5g5F8XlNRdD1xmVZXU8X2-xh9LqRpnEakdhecciFHg0u6AyC4c00rlo_HBb69wlXajQ3R4y26Kpxn7HA";

    let jwt = key_store.decode(token).unwrap();

    let claims = jwt.payload().into::<MyClaims>().unwrap();

    println!("Issuer: {}", claims.iss);
    println!("Name: {}", claims.name);
    println!("Email: {}", claims.email);
}

历史

  • 0.2.0

  • 0.1.8

  • 0.1.7

    • 更新了依赖项
  • 0.1.6

    • 添加了 key_set::KeyStore::should_refresh() 以测试是否应该刷新密钥
    • 添加了 key_set::KeyStore::refresh_interval 以确定在密钥过期之前多早应该刷新密钥
    • 一些额外的文档
  • 0.1.5:

    • readme = "README.md" 添加到 Cargo.toml
  • 0.1.4:

    • 更新了文档——特别是如何使用 JWKS-Client 与 Rocket
    • 添加了从 KeyStore 确定是否应该刷新密钥的功能
    • 修复了此页面上的示例——它们现在直接来自 ./examples/*
  • 0.1.3:

    • 将许可证更改为 MIT/Apache
    • 将示例移动到 ./example
    • 添加了根据缓存控制头验证密钥是否需要在密钥存储中刷新的功能
  • 0.1.2:(对于破坏性变更表示歉意)

    • 将模块 jwks 重命名为 keyset
    • 将结构体 Jwks 重命名为 KeyStore
    • 略微扩展了文档
    • 修复了一些示例
  • 0.1.1:原始版本

待办事项

  • 更多的文档 :)
  • 自动刷新密钥

(使用 ❤️ 和 Rust 制作)

依赖项

~11–17MB
~427K SLoC