Skip to content

Commit

Permalink
test: add test for token creation
Browse files Browse the repository at this point in the history
  • Loading branch information
its-danny committed Aug 19, 2024
1 parent 5c53ab4 commit 63f01fc
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 36 deletions.
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use http::StatusCode;

#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum Error {
/// Maps the [`hmac::digest::InvalidLength`] error.
#[error(transparent)]
Expand Down
18 changes: 2 additions & 16 deletions src/guard.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
use base64::prelude::*;
use futures_util::future::BoxFuture;
use hmac::Mac;
use http::{Method, Request, Response};
use std::{
sync::Arc,
Expand All @@ -10,7 +8,7 @@ use tower_cookies::Cookies;
use tower_layer::Layer;
use tower_service::Service;

use crate::{surf::Config, Error, HmacSha256};
use crate::{surf::Config, token::validate_token, Error};

#[derive(Clone, Default)]
pub struct Guard;
Expand All @@ -32,18 +30,6 @@ impl<S> GuardService<S> {
pub(crate) fn new(inner: S) -> Self {
Self { inner }
}

pub(crate) fn validate(secret: &str, cookie: &str, token: &str) -> Result<bool, Error> {
let mut parts = token.splitn(2, '.');
let received_hmac = parts.next().unwrap_or("");

let mut mac = HmacSha256::new_from_slice(secret.as_bytes())?;
let message = parts.next().unwrap_or("");
mac.update(message.as_bytes());
let expected_hmac = BASE64_STANDARD.encode(mac.finalize().into_bytes());

Ok(received_hmac == expected_hmac && cookie == token)
}
}

impl<S, Q, R> Service<Request<Q>> for GuardService<S>
Expand Down Expand Up @@ -106,7 +92,7 @@ where
None => return Error::make_layer_forbidden(),
};

match GuardService::<S>::validate(&config.secret, &cookie_value, &header_value) {
match validate_token(&config.secret, &cookie_value, &header_value) {
Ok(valid) => {
if valid {
Ok(response)
Expand Down
5 changes: 0 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,6 @@
//! [owasp-double-submit]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#alternative-using-a-double-submit-cookie-pattern
//! [owasp-login-csrf]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#possible-csrf-vulnerabilities-in-login-forms
use hmac::Hmac;
use sha2::Sha256;

pub(crate) type HmacSha256 = Hmac<Sha256>;

pub use error::Error;
pub use surf::Surf;
pub use token::Token;
Expand Down
79 changes: 65 additions & 14 deletions src/token.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::sync::Arc;

use base64::prelude::*;
use hmac::Mac;
use hmac::{Hmac, Mac};
use rand::prelude::*;
use sha2::Sha256;
use tower_cookies::{Cookie, Cookies};

use crate::{error::Error, surf::Config, HmacSha256};
use crate::{error::Error, surf::Config};

/// An extension providing a way to interact with a visitor's
/// CSRF token.
Expand All @@ -18,7 +19,7 @@ pub struct Token {
impl Token {
pub(crate) fn create(&self) -> Result<(), Error> {
let identifier: i128 = thread_rng().gen();
let token = self.sign(identifier.to_string())?;
let token = create_token(&self.config.secret, identifier.to_string())?;

let cookie = Cookie::build((self.config.cookie_name(), token))
.path("/")
Expand All @@ -42,7 +43,7 @@ impl Token {
///
/// - [`Error::InvalidLength`]
pub fn set(&self, identifier: impl Into<String>) -> Result<(), Error> {
let token = self.sign(identifier)?;
let token = create_token(&self.config.secret, identifier)?;

let cookie = Cookie::build((self.config.cookie_name(), token))
.path("/")
Expand Down Expand Up @@ -75,19 +76,69 @@ impl Token {

self.cookies.remove(cookie);
}
}

type HmacSha256 = Hmac<Sha256>;

pub(crate) fn create_token(secret: &str, identifier: impl Into<String>) -> Result<String, Error> {
let random = BASE64_STANDARD.encode(get_random_value());
let message = format!("{}!{}", identifier.into(), random);
let result = sign_and_encode(secret, &message)?;
let token = format!("{}.{}", result, message);

fn sign(&self, identifier: impl Into<String>) -> Result<String, Error> {
let mut random = [0u8; 64];
thread_rng().fill(&mut random);
let random = BASE64_STANDARD.encode(random);
Ok(token)
}

pub(crate) fn validate_token(secret: &str, cookie: &str, token: &str) -> Result<bool, Error> {
let mut parts = token.splitn(2, '.');
let received_hmac = parts.next().unwrap_or("");

let message = format!("{}!{}", identifier.into(), random);
let mut mac = HmacSha256::new_from_slice(self.config.secret.as_bytes())?;
mac.update(message.as_bytes());
let result = BASE64_STANDARD.encode(mac.finalize().into_bytes());
let message = parts.next().unwrap_or("");
let expected_hmac = sign_and_encode(secret, message)?;

let token = format!("{}.{}", result, message);
Ok(received_hmac == expected_hmac && cookie == token)
}

#[cfg(not(test))]
fn get_random_value() -> [u8; 64] {
let mut random = [0u8; 64];
thread_rng().fill(&mut random);

random
}

#[cfg(test)]
fn get_random_value() -> [u8; 64] {
[42u8; 64]
}

Ok(token)
fn sign_and_encode(secret: &str, message: &str) -> Result<String, Error> {
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())?;
mac.update(message.as_bytes());
let result = BASE64_STANDARD.encode(mac.finalize().into_bytes());

Ok(result)
}

#[cfg(test)]
mod tests {
use anyhow::Result;

use super::*;

#[test]
fn create_token() -> Result<()> {
let token = super::create_token("super-secret", "identifier")?;

let parts = token.splitn(2, '.').collect::<Vec<&str>>();
assert_eq!(parts.len(), 2);

let message = format!("{}!{}", "identifier", BASE64_STANDARD.encode([42u8; 64]));
assert_eq!(parts[1], message);

let signature = sign_and_encode("super-secret", &message)?;
assert_eq!(parts[0], signature);

Ok(())
}
}

0 comments on commit 63f01fc

Please sign in to comment.