chore: initial commit
This commit is contained in:
parent
b1a0c97cab
commit
0bf2b5f63b
7
.gitignore
vendored
7
.gitignore
vendored
@ -18,4 +18,9 @@ Cargo.lock
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
#.idea/
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
.vscode/
|
||||
|
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "paseto_maker"
|
||||
authors = ["itsscb <dev@itsscb.de>"]
|
||||
license = "GPL-3.0"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/itsscb/paseto_maker"
|
||||
description = "This library provides high-level functionality for creating, handling, and managing PASETO tokens."
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.39", features = ["serde"] }
|
||||
ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
|
||||
rand = "0.8.5"
|
||||
rusty_paseto = { version = "0.7.2", features = [
|
||||
"batteries_included",
|
||||
"v4_public",
|
||||
] }
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.134"
|
||||
thiserror = "2.0.9"
|
46
README.md
46
README.md
@ -1,2 +1,46 @@
|
||||
# paseto_maker
|
||||
Rust lib to create and verify paseto tokens and add and extract claims
|
||||
|
||||
This library provides high-level functionality for creating, handling, and managing PASETO tokens.
|
||||
|
||||
**Note:** This crate is currently in Alpha. The API is subject to change and may contain bugs.
|
||||
|
||||
# Overview
|
||||
|
||||
This library includes modules for defining claims, handling errors, and creating/verifying PASETO tokens.
|
||||
It leverages the `rusty_paseto` crate and currently supports PASETO Tokens V4.public.
|
||||
|
||||
# Modules
|
||||
|
||||
- `claims`: Defines the structure and behavior of the claims that can be embedded in a PASETO token.
|
||||
- `errors`: Provides error types and handling mechanisms for the library.
|
||||
- `maker`: Contains the logic for creating and verifying PASETO tokens.
|
||||
|
||||
# Re-exports
|
||||
|
||||
- `Claims`: The struct representing the claims in a PASETO token.
|
||||
- `Maker`: The struct used for creating and verifying PASETO tokens.
|
||||
|
||||
# Usage Example
|
||||
|
||||
```rust
|
||||
use paseto_maker::{Maker, Claims, version::V4, purpose::Public};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let maker = Maker::new_with_keypair().unwrap();
|
||||
let claims = Claims::new().with_subject("example");
|
||||
let token = maker.create_token(&claims).unwrap();
|
||||
println!("Token: {}", token);
|
||||
|
||||
let verified_claims = maker.verify_token(&token)?;
|
||||
println!("Verified Claims: {:?}", verified_claims);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
The `claims` module defines the structure and behavior of the claims that can be embedded in a PASETO token.
|
||||
The `errors` module provides error types and handling mechanisms for the library.
|
||||
The `maker` module contains the logic for creating and verifying PASETO tokens.
|
||||
|
||||
The `Claims` struct and `Maker` struct are re-exported for ease of use.
|
||||
|
||||
This library uses the `rusty_paseto` crate underneath and currently only supports PASETO Tokens V4.public.
|
||||
|
96
flake.lock
generated
Normal file
96
flake.lock
generated
Normal file
@ -0,0 +1,96 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1736042175,
|
||||
"narHash": "sha256-jdd5UWtLVrNEW8K6u5sy5upNAFmF3S4Y+OIeToqJ1X8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "bf689c40d035239a489de5997a4da5352434632e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1728538411,
|
||||
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1736130662,
|
||||
"narHash": "sha256-z+WGez9oTR2OsiUWE5ZhIpETqM1ogrv6Xcd24WFi6KQ=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "2f5d4d9cd31cc02c36e51cb2e21c4b25c4f78c52",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
51
flake.nix
Normal file
51
flake.nix
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
description = "Example Rust development environment for Zero to Nix";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
};
|
||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
||||
extensions = [ "rust-src" "rust-analyzer" "clippy" "rustfmt" ];
|
||||
targets = [ "x86_64-unknown-linux-gnu" ];
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
rustToolchain
|
||||
clippy
|
||||
cargo-edit
|
||||
cargo-binstall
|
||||
bacon
|
||||
openssl
|
||||
pkg-config
|
||||
# Add rust-analyzer separately to ensure it's the latest version
|
||||
# rust-analyzer
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export PATH=${rustToolchain}/bin:$PATH
|
||||
export RUSTC_VERSION=$(rustc --version)
|
||||
export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"
|
||||
export OPENSSL_DIR="${pkgs.openssl.dev}"
|
||||
export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"
|
||||
export OPENSSL_INCLUDE_DIR="${pkgs.openssl.dev}/include"
|
||||
# Add this line to explicitly set the rust-analyzer path
|
||||
export RUST_ANALYZER_PATH="${pkgs.rust-analyzer}/bin/rust-analyzer"
|
||||
'';
|
||||
|
||||
packages = pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [ libiconv ]);
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
326
src/claims/mod.rs
Normal file
326
src/claims/mod.rs
Normal file
@ -0,0 +1,326 @@
|
||||
use crate::errors::ClaimError;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt::{self, Display, Formatter},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub mod reserved;
|
||||
|
||||
/// Represents a collection of claims for a token.
|
||||
///
|
||||
/// Claims are stored in a `BTreeMap` with `Arc<str>` keys and `serde_json::Value` values.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use paseto_maker::Claims;
|
||||
///
|
||||
/// let claims = Claims::new()
|
||||
/// .with_subject("1234567890")
|
||||
/// .with_issuer("issuer")
|
||||
/// .with_audience("audience")
|
||||
/// .with_expiration("2023-10-01T00:00:00+00:00")
|
||||
/// .with_not_before("2023-09-01T00:00:00+00:00")
|
||||
/// .with_issued_at("2023-09-01T00:00:00+00:00")
|
||||
/// .with_token_identifier("token_id");
|
||||
///
|
||||
/// let subject: Option<String> = claims.get_subject();
|
||||
/// assert_eq!(subject, Some("1234567890".to_string()));
|
||||
/// ```
|
||||
///
|
||||
/// # Methods
|
||||
///
|
||||
/// - `new`: Creates a new, empty `Claims` instance.
|
||||
/// - `with_subject`: Adds a subject claim.
|
||||
/// - `with_issuer`: Adds an issuer claim.
|
||||
/// - `with_audience`: Adds an audience claim.
|
||||
/// - `with_expiration`: Adds an expiration claim.
|
||||
/// - `with_not_before`: Adds a not-before claim.
|
||||
/// - `with_issued_at`: Adds an issued-at claim.
|
||||
/// - `with_token_identifier`: Adds a token identifier claim.
|
||||
/// - `set_claim`: Sets a custom claim with a specified key and value.
|
||||
/// - `get_claim`: Retrieves a claim by key and attempts to deserialize it into the specified type.
|
||||
/// - `get_subject`: Retrieves the subject claim.
|
||||
/// - `get_issuer`: Retrieves the issuer claim.
|
||||
/// - `get_audience`: Retrieves the audience claim.
|
||||
/// - `get_expiration`: Retrieves the expiration claim.
|
||||
/// - `get_not_before`: Retrieves the not-before claim.
|
||||
/// - `get_issued_at`: Retrieves the issued-at claim.
|
||||
/// - `get_token_identifier`: Retrieves the token identifier claim.
|
||||
/// - `iter`: Returns an iterator over the claims.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - `set_claim` will return an error if the value cannot be serialized or is null.
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use paseto_maker::Claims;
|
||||
///
|
||||
/// let mut claims = Claims::new();
|
||||
/// claims.set_claim("sub", "1234567890").unwrap();
|
||||
/// claims.set_claim("name", "John Doe").unwrap();
|
||||
/// claims.set_claim("admin", true).unwrap();
|
||||
///
|
||||
/// let sub: Option<String> = claims.get_claim("sub");
|
||||
/// assert_eq!(sub, Some("1234567890".to_string()));
|
||||
///
|
||||
/// let name: Option<String> = claims.get_claim("name");
|
||||
/// assert_eq!(name, Some("John Doe".to_string()));
|
||||
///
|
||||
/// let admin: Option<bool> = claims.get_claim("admin");
|
||||
/// assert_eq!(admin, Some(true));
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// use paseto_maker::Claims;
|
||||
/// use chrono::{DateTime, Utc};
|
||||
///
|
||||
/// let claims = Claims::new()
|
||||
/// .with_subject("1234567890")
|
||||
/// .with_issuer("issuer")
|
||||
/// .with_audience("audience")
|
||||
/// .with_expiration("2023-10-01T00:00:00+00:00")
|
||||
/// .with_not_before("2023-09-01T00:00:00+00:00")
|
||||
/// .with_issued_at("2023-09-01T00:00:00+00:00")
|
||||
/// .with_token_identifier("token_id");
|
||||
///
|
||||
/// let subject: Option<String> = claims.get_subject();
|
||||
/// assert_eq!(subject, Some("1234567890".to_string()));
|
||||
///
|
||||
/// let issuer: Option<String> = claims.get_issuer();
|
||||
/// assert_eq!(issuer, Some("issuer".to_string()));
|
||||
///
|
||||
/// let audience: Option<String> = claims.get_audience();
|
||||
/// assert_eq!(audience, Some("audience".to_string()));
|
||||
///
|
||||
/// let expiration: Option<DateTime<Utc>> = claims.get_expiration();
|
||||
/// assert_eq!(expiration.unwrap().to_rfc3339().to_string(), "2023-10-01T00:00:00+00:00".to_string());
|
||||
///
|
||||
/// let not_before: Option<DateTime<Utc>> = claims.get_not_before();
|
||||
/// assert_eq!(not_before.unwrap().to_rfc3339().to_string(), "2023-09-01T00:00:00+00:00".to_string());
|
||||
///
|
||||
/// let issued_at: Option<DateTime<Utc>> = claims.get_issued_at();
|
||||
/// assert_eq!(issued_at.unwrap().to_rfc3339().to_string(), "2023-09-01T00:00:00+00:00".to_string());
|
||||
///
|
||||
/// let token_identifier: Option<String> = claims.get_token_identifier();
|
||||
/// assert_eq!(token_identifier, Some("token_id".to_string()));
|
||||
/// ```
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Claims {
|
||||
claims: BTreeMap<Arc<str>, Value>,
|
||||
}
|
||||
|
||||
impl Display for Claims {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let claims: Vec<String> = self
|
||||
.claims
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{k}: {v}"))
|
||||
.collect();
|
||||
write!(f, "{}", claims.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value> for Claims {
|
||||
fn from(value: Value) -> Self {
|
||||
let claims: BTreeMap<Arc<str>, Value> = match value {
|
||||
Value::Object(map) => map.into_iter().map(|(k, v)| (Arc::from(k), v)).collect(),
|
||||
_ => BTreeMap::new(),
|
||||
};
|
||||
Self { claims }
|
||||
}
|
||||
}
|
||||
|
||||
impl Claims {
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&Arc<str>, &Value)> {
|
||||
self.claims.iter()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_subject<T: AsRef<str>>(mut self, subject: T) -> Self {
|
||||
self.claims
|
||||
.insert(Arc::from(reserved::SUBJECT), subject.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_issuer<T: AsRef<str>>(mut self, issuer: T) -> Self {
|
||||
self.claims
|
||||
.insert(Arc::from(reserved::ISSUER), issuer.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_audience<T: AsRef<str>>(mut self, audience: T) -> Self {
|
||||
self.claims
|
||||
.insert(Arc::from(reserved::AUDIENCE), audience.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_expiration<T: AsRef<str>>(mut self, expiration: T) -> Self {
|
||||
self.claims
|
||||
.insert(Arc::from(reserved::EXPIRATION), expiration.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_not_before<T: AsRef<str>>(mut self, not_before: T) -> Self {
|
||||
self.claims
|
||||
.insert(Arc::from(reserved::NOT_BEFORE), not_before.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_issued_at<T: AsRef<str>>(mut self, issued_at: T) -> Self {
|
||||
self.claims
|
||||
.insert(Arc::from(reserved::ISSUED_AT), issued_at.as_ref().into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_token_identifier<T: AsRef<str>>(mut self, token_identifier: T) -> Self {
|
||||
self.claims.insert(
|
||||
Arc::from(reserved::TOKEN_IDENTIFIER),
|
||||
token_identifier.as_ref().into(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the value cannot be serialized.
|
||||
/// * `get_claim` - Retrieves a claim by key and attempts to deserialize it into the specified type.
|
||||
///
|
||||
pub fn set_claim<T: Serialize>(&mut self, key: &str, value: T) -> Result<(), ClaimError> {
|
||||
let value = serde_json::to_value(value)?;
|
||||
if value.is_null() {
|
||||
return Err(ClaimError::InvalidValue);
|
||||
}
|
||||
self.claims.insert(Arc::from(key), value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_claim<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
|
||||
self.claims
|
||||
.get(&Arc::from(key))
|
||||
.and_then(|value| serde_json::from_value(value.clone()).ok())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_subject(&self) -> Option<String> {
|
||||
self.get_claim(reserved::SUBJECT)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_issuer(&self) -> Option<String> {
|
||||
self.get_claim(reserved::ISSUER)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_audience(&self) -> Option<String> {
|
||||
self.get_claim(reserved::AUDIENCE)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_expiration(&self) -> Option<DateTime<Utc>> {
|
||||
self.get_claim(reserved::EXPIRATION)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_not_before(&self) -> Option<DateTime<Utc>> {
|
||||
self.get_claim(reserved::NOT_BEFORE)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_issued_at(&self) -> Option<DateTime<Utc>> {
|
||||
self.get_claim(reserved::ISSUED_AT)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_token_identifier(&self) -> Option<String> {
|
||||
self.get_claim(reserved::TOKEN_IDENTIFIER)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_set_and_get_claim() {
|
||||
let mut claims = Claims::new();
|
||||
claims.set_claim("sub", "1234567890").unwrap();
|
||||
claims.set_claim("name", "John Doe").unwrap();
|
||||
claims.set_claim("admin", true).unwrap();
|
||||
|
||||
let sub: Option<String> = claims.get_claim("sub");
|
||||
assert_eq!(sub, Some("1234567890".to_string()));
|
||||
|
||||
let name: Option<String> = claims.get_claim("name");
|
||||
assert_eq!(name, Some("John Doe".to_string()));
|
||||
|
||||
let admin: Option<bool> = claims.get_claim("admin");
|
||||
assert_eq!(admin, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder() {
|
||||
let claims = Claims::new()
|
||||
.with_subject("1234567890")
|
||||
.with_audience("test audience")
|
||||
.with_issued_at("2019-01-01T00:00:00+00:00");
|
||||
|
||||
let sub: Option<String> = claims.get_claim("sub");
|
||||
assert_eq!(sub, Some("1234567890".to_string()));
|
||||
let sub: Option<String> = claims.get_claim("aud");
|
||||
assert_eq!(sub, Some("test audience".to_string()));
|
||||
|
||||
let sub: Option<String> = claims.get_claim("iat");
|
||||
assert_eq!(sub, Some("2019-01-01T00:00:00+00:00".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter() {
|
||||
let mut claims = Claims::new();
|
||||
claims.set_claim("sub", "1234567890").unwrap();
|
||||
claims.set_claim("name", "John Doe").unwrap();
|
||||
claims.set_claim("admin", true).unwrap();
|
||||
|
||||
let mut iter = claims.iter();
|
||||
assert_eq!(iter.next(), Some((&Arc::from("admin"), &json!(true))));
|
||||
assert_eq!(iter.next(), Some((&Arc::from("name"), &json!("John Doe"))));
|
||||
assert_eq!(iter.next(), Some((&Arc::from("sub"), &json!("1234567890"))));
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_claim_nonexistent() {
|
||||
let claims = Claims::new();
|
||||
let value: Option<String> = claims.get_claim("nonexistent");
|
||||
assert_eq!(value, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_claim_error() {
|
||||
let mut claims = Claims::new();
|
||||
let result = claims.set_claim("invalid", f64::NAN);
|
||||
dbg!(&result);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
7
src/claims/reserved.rs
Normal file
7
src/claims/reserved.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub const ISSUER: &str = "iss";
|
||||
pub const SUBJECT: &str = "sub";
|
||||
pub const AUDIENCE: &str = "aud";
|
||||
pub const EXPIRATION: &str = "exp";
|
||||
pub const NOT_BEFORE: &str = "nbf";
|
||||
pub const ISSUED_AT: &str = "iat";
|
||||
pub const TOKEN_IDENTIFIER: &str = "jti";
|
38
src/errors.rs
Normal file
38
src/errors.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ClaimError {
|
||||
#[error("invalid value")]
|
||||
InvalidValue,
|
||||
#[error("serialization error: {0}")]
|
||||
SerializationError(#[from] serde_json::Error),
|
||||
#[error("paseto error: {0}")]
|
||||
PasetoError(#[from] rusty_paseto::generic::PasetoError),
|
||||
#[error("invalid claim: {0}")]
|
||||
InvalidClaim(#[from] rusty_paseto::generic::PasetoClaimError),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TokenError {
|
||||
#[error("Invalid claim: {0}")]
|
||||
InvalidClaim(String),
|
||||
#[error("Token expired")]
|
||||
Expired,
|
||||
#[error("Token not valid")]
|
||||
Invalid,
|
||||
#[error("Token validation failed")]
|
||||
Validation,
|
||||
#[error("Token malformed")]
|
||||
Format,
|
||||
#[error("Claim error: {0}")]
|
||||
ClaimError(#[from] ClaimError),
|
||||
#[error("Token creation failed: {0}")]
|
||||
TokenCreationFailed(String),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MakerError {
|
||||
#[error("Invalid key: {0}")]
|
||||
InvalidKey(String),
|
||||
}
|
49
src/lib.rs
Normal file
49
src/lib.rs
Normal file
@ -0,0 +1,49 @@
|
||||
//! This library provides high-level functionality for creating, handling, and managing PASETO tokens.
|
||||
//!
|
||||
//! **Note:** This crate is currently in Alpha. The API is subject to change and may contain bugs.
|
||||
//!
|
||||
//! # Overview
|
||||
//! This library includes modules for defining claims, handling errors, and creating/verifying PASETO tokens.
|
||||
//! It leverages the `rusty_paseto` crate and currently supports PASETO Tokens V4.public.
|
||||
//!
|
||||
//! # Modules
|
||||
//! - `claims`: Defines the structure and behavior of the claims that can be embedded in a PASETO token.
|
||||
//! - `errors`: Provides error types and handling mechanisms for the library.
|
||||
//! - `maker`: Contains the logic for creating and verifying PASETO tokens.
|
||||
//!
|
||||
//! # Re-exports
|
||||
//! - `Claims`: The struct representing the claims in a PASETO token.
|
||||
//! - `Maker`: The struct used for creating and verifying PASETO tokens.
|
||||
//!
|
||||
//! # Usage Example
|
||||
//! ```rust
|
||||
//! use paseto_maker::{Maker, Claims, version::V4, purpose::Public};
|
||||
//!
|
||||
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let maker = Maker::new_with_keypair().unwrap();
|
||||
//! let claims = Claims::new().with_subject("example");
|
||||
//! let token = maker.create_token(&claims).unwrap();
|
||||
//! println!("Token: {}", token);
|
||||
//!
|
||||
//! let verified_claims = maker.verify_token(&token)?;
|
||||
//! println!("Verified Claims: {:?}", verified_claims);
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! The `claims` module defines the structure and behavior of the claims that can be embedded in a PASETO token.
|
||||
//! The `errors` module provides error types and handling mechanisms for the library.
|
||||
//! The `maker` module contains the logic for creating and verifying PASETO tokens.
|
||||
//!
|
||||
//! The `Claims` struct and `Maker` struct are re-exported for ease of use.
|
||||
//!
|
||||
//! This library uses the `rusty_paseto` crate underneath and currently only supports PASETO Tokens V4.public.
|
||||
pub(crate) mod claims;
|
||||
pub mod errors;
|
||||
pub(crate) mod maker;
|
||||
|
||||
pub use claims::Claims;
|
||||
pub use maker::Maker;
|
||||
|
||||
pub mod purpose;
|
||||
pub mod version;
|
311
src/maker/mod.rs
Normal file
311
src/maker/mod.rs
Normal file
@ -0,0 +1,311 @@
|
||||
#![allow(dead_code)]
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use rusty_paseto::{
|
||||
core::{
|
||||
Key, PasetoAsymmetricPrivateKey, PasetoAsymmetricPublicKey, Public as pPublic, V4 as pV4,
|
||||
},
|
||||
prelude::{
|
||||
AudienceClaim, CustomClaim, ExpirationClaim, IssuedAtClaim, IssuerClaim, NotBeforeClaim,
|
||||
PasetoBuilder, SubjectClaim, TokenIdentifierClaim,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{claims::reserved, purpose::Public, version::V4, Claims};
|
||||
use crate::{
|
||||
errors::{MakerError, TokenError},
|
||||
purpose::Purpose,
|
||||
version::Version,
|
||||
};
|
||||
// pub mod error;
|
||||
|
||||
pub struct Maker<V: Version, P: Purpose> {
|
||||
private_key: Key<64>,
|
||||
public_key: Key<32>,
|
||||
public_key_bytes: [u8; 32],
|
||||
version: String,
|
||||
purpose: String,
|
||||
_version: PhantomData<V>,
|
||||
_purpose: PhantomData<P>,
|
||||
}
|
||||
|
||||
/// `Maker` is a struct that provides functionality to create and manage PASETO (Platform-Agnostic Security Tokens) tokens.
|
||||
///
|
||||
/// # Methods
|
||||
///
|
||||
/// - `new(private_key: &[u8; 64]) -> Self`
|
||||
/// - Creates a new `Maker` instance with the given private and public keys.
|
||||
/// - `new_with_keypair() -> Self`
|
||||
/// - Generates a new keypair and creates a new `Maker` instance with the generated keys.
|
||||
/// - `new_keypair() -> ([u8; 64], [u8; 32])`
|
||||
/// - Generates a new Ed25519 keypair and returns the private and public keys.
|
||||
/// - `private_key(&self) -> PasetoAsymmetricPrivateKey<V4, Public>`
|
||||
/// - Returns the private key as a `PasetoAsymmetricPrivateKey`.
|
||||
/// - `public_key(&self) -> PasetoAsymmetricPublicKey<V4, Public>`
|
||||
/// - Returns the public key as a `PasetoAsymmetricPublicKey`.
|
||||
/// - `create_token(&self, claims: &Claims) -> Result<String, TokenError>`
|
||||
/// - Creates a new PASETO token with the given claims. Returns the token as a `String` or an error if the token creation fails.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use paseto_maker::{Maker, Claims, version::V4, purpose::Public};
|
||||
/// let maker = Maker::new_with_keypair().unwrap();
|
||||
/// let claims = Claims::new();
|
||||
/// let token = maker.create_token(&claims).unwrap();
|
||||
/// ```
|
||||
impl Maker<V4, Public> {
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the provided private key is invalid.
|
||||
pub fn new(private_key: &[u8; 64]) -> Result<Self, MakerError> {
|
||||
let private_key = ed25519_dalek::SigningKey::from_keypair_bytes(private_key)
|
||||
.map_err(|err| MakerError::InvalidKey(err.to_string()))?;
|
||||
let public_key = private_key.verifying_key().to_bytes();
|
||||
Ok(Self {
|
||||
private_key: Key::<64>::from(&private_key.to_keypair_bytes()),
|
||||
public_key: Key::<32>::from(&public_key),
|
||||
public_key_bytes: public_key,
|
||||
version: V4::NAME.to_string(),
|
||||
purpose: Public::NAME.to_string(),
|
||||
_version: PhantomData,
|
||||
_purpose: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the key generation or Maker creation fails.
|
||||
pub fn new_with_keypair() -> Result<Self, MakerError> {
|
||||
// let (private_key, public_key) = Self::new_keypair();
|
||||
let private_key = Self::new_private_key();
|
||||
Self::new(&private_key)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new_private_key() -> [u8; 64] {
|
||||
let mut csprng = rand::rngs::OsRng;
|
||||
let priv_key: ed25519_dalek::SigningKey = ed25519_dalek::SigningKey::generate(&mut csprng);
|
||||
priv_key.to_keypair_bytes()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new_keypair() -> ([u8; 64], [u8; 32]) {
|
||||
let mut csprng = rand::rngs::OsRng;
|
||||
let priv_key: ed25519_dalek::SigningKey = ed25519_dalek::SigningKey::generate(&mut csprng);
|
||||
let pub_key = priv_key.verifying_key();
|
||||
(priv_key.to_keypair_bytes(), pub_key.to_bytes())
|
||||
}
|
||||
|
||||
fn private_key(&self) -> PasetoAsymmetricPrivateKey<pV4, pPublic> {
|
||||
PasetoAsymmetricPrivateKey::<pV4, pPublic>::from(&self.private_key)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn public_key(&self) -> PasetoAsymmetricPublicKey<pV4, pPublic> {
|
||||
PasetoAsymmetricPublicKey::<pV4, pPublic>::from(&self.public_key)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn public_key_as_bytes(&self) -> &[u8; 32] {
|
||||
&self.public_key_bytes
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the token verification fails.
|
||||
pub fn verify_token(&self, token: &str) -> Result<Claims, TokenError> {
|
||||
let public_key = self.public_key();
|
||||
let mut parser = rusty_paseto::prelude::PasetoParser::<pV4, pPublic>::default();
|
||||
|
||||
let token = {
|
||||
parser
|
||||
.parse(token, &public_key)
|
||||
.map_err(|err| TokenError::TokenCreationFailed(err.to_string()))?
|
||||
};
|
||||
Ok(token.into())
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the token creation fails due to invalid claims or other issues.
|
||||
///
|
||||
pub fn create_token(&self, claims: &Claims) -> Result<String, TokenError> {
|
||||
let mut builder = PasetoBuilder::<pV4, pPublic>::default();
|
||||
|
||||
for (key, value) in claims.iter() {
|
||||
// dbg!(key, format!("{}", value.to_string().trim_matches('"').to_string()));
|
||||
match key.as_ref() {
|
||||
reserved::ISSUER => {
|
||||
if let Some(issuer) = value.as_str() {
|
||||
let _ = builder.set_claim(IssuerClaim::from(issuer));
|
||||
} else {
|
||||
return Err(TokenError::InvalidClaim("Invalid issuer claim".to_string()));
|
||||
}
|
||||
}
|
||||
reserved::AUDIENCE => {
|
||||
if let Some(audience) = value.as_str() {
|
||||
let _ = builder.set_claim(AudienceClaim::from(audience));
|
||||
} else {
|
||||
return Err(TokenError::InvalidClaim(
|
||||
"Invalid audience claim".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
reserved::SUBJECT => {
|
||||
if let Some(subject) = value.as_str() {
|
||||
let _ = builder.set_claim(SubjectClaim::from(subject));
|
||||
} else {
|
||||
return Err(TokenError::InvalidClaim(
|
||||
"Invalid subject claim".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
reserved::ISSUED_AT => {
|
||||
if let Some(issued_at) = value.as_str() {
|
||||
match IssuedAtClaim::try_from(issued_at) {
|
||||
Ok(claim) => {
|
||||
let _ = builder.set_claim(claim);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TokenError::ClaimError(err.into()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(TokenError::InvalidClaim(
|
||||
"Invalid issued at claim".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
reserved::NOT_BEFORE => {
|
||||
if let Some(not_before) = value.as_str() {
|
||||
match NotBeforeClaim::try_from(not_before) {
|
||||
Ok(claim) => {
|
||||
let _ = builder.set_claim(claim);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TokenError::ClaimError(err.into()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(TokenError::InvalidClaim(
|
||||
"Invalid not before claim".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
reserved::EXPIRATION => {
|
||||
if let Some(expiration) = value.as_str() {
|
||||
match ExpirationClaim::try_from(expiration) {
|
||||
Ok(claim) => {
|
||||
let _ = builder.set_claim(claim);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TokenError::ClaimError(err.into()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(TokenError::InvalidClaim(
|
||||
"Invalid expiration claim".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
reserved::TOKEN_IDENTIFIER => {
|
||||
let claim = match value.as_str() {
|
||||
Some(token_id) => TokenIdentifierClaim::from(token_id),
|
||||
None => {
|
||||
return Err(TokenError::InvalidClaim(
|
||||
"Invalid token identifier claim".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
let _ = builder.set_claim(claim);
|
||||
}
|
||||
key => match CustomClaim::try_from((key, value)) {
|
||||
Ok(claim) => {
|
||||
let _ = builder.set_claim(claim);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TokenError::InvalidClaim(err.to_string()));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
.build(&self.private_key())
|
||||
.map_err(|err| TokenError::TokenCreationFailed(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
};
|
||||
|
||||
use rusty_paseto::prelude::PasetoParser;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_invalid_claims() {
|
||||
let maker = Maker::new_with_keypair().expect("failed to create maker");
|
||||
|
||||
let claims = Claims::new().with_issued_at("invalid RF3339 date");
|
||||
let token = maker.create_token(&claims);
|
||||
assert!(token.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_token() {
|
||||
let maker = Maker::new_with_keypair().expect("failed to create maker");
|
||||
let public_key = maker.public_key();
|
||||
let mut claims = Claims::new().with_issued_at("2027-09-18T03:42:15+02:00");
|
||||
claims.set_claim("sub", "this is the subject").unwrap();
|
||||
claims.set_claim("data", "test").unwrap();
|
||||
claims.set_claim("number", 2).unwrap();
|
||||
|
||||
let token = maker
|
||||
.create_token(&claims)
|
||||
.expect("failed to generate token");
|
||||
|
||||
let got = maker.verify_token(&token).expect("failed to verify token");
|
||||
|
||||
assert_eq!(got.get_subject().unwrap().as_str(), "this is the subject");
|
||||
let mut parser = PasetoParser::<pV4, pPublic>::default();
|
||||
let token = parser
|
||||
.parse(&token, &public_key)
|
||||
.expect("failed to parse token");
|
||||
dbg!(&token);
|
||||
|
||||
assert!(token.get("sub").is_some());
|
||||
assert_eq!(
|
||||
token.get("sub").unwrap().as_str().unwrap(),
|
||||
"this is the subject"
|
||||
);
|
||||
assert!(token.get("number").is_some());
|
||||
assert_eq!(token.get("number").unwrap(), 2);
|
||||
assert!(token.get("data").is_some());
|
||||
assert_eq!(token.get("data").unwrap(), "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_private_key() {
|
||||
let new_key = Key::<64>::try_new_random().unwrap();
|
||||
// let private_key = PasetoAsymmetricPrivateKey::<V4, Public>::from(&new_key);
|
||||
let mut file = File::create("temp_dev_private_key").unwrap();
|
||||
file.write_all(new_key.as_slice()).unwrap();
|
||||
|
||||
let mut file = File::open("temp_dev_private_key").unwrap();
|
||||
let mut private_key = Vec::new();
|
||||
file.read_to_end(&mut private_key).unwrap();
|
||||
|
||||
let got_key = Key::<64>::from(private_key.as_slice());
|
||||
|
||||
assert_eq!(got_key.as_slice(), new_key.as_slice());
|
||||
|
||||
std::fs::remove_file("temp_dev_private_key").unwrap();
|
||||
}
|
||||
}
|
3
src/purpose/mod.rs
Normal file
3
src/purpose/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub trait Purpose {}
|
||||
mod public;
|
||||
pub use public::Public;
|
8
src/purpose/public.rs
Normal file
8
src/purpose/public.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use super::Purpose;
|
||||
|
||||
pub struct Public;
|
||||
impl Public {
|
||||
pub const NAME: &'static str = "public";
|
||||
}
|
||||
|
||||
impl Purpose for Public {}
|
3
src/version/mod.rs
Normal file
3
src/version/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub trait Version {}
|
||||
mod v4;
|
||||
pub use v4::V4;
|
7
src/version/v4.rs
Normal file
7
src/version/v4.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use super::Version;
|
||||
|
||||
pub struct V4;
|
||||
impl V4 {
|
||||
pub const NAME: &'static str = "v4";
|
||||
}
|
||||
impl Version for V4 {}
|
Loading…
x
Reference in New Issue
Block a user