Skip to content

Commit

Permalink
Test token hashing
Browse files Browse the repository at this point in the history
  • Loading branch information
sdankel committed May 2, 2024
1 parent d1c1dab commit 45384af
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 26 deletions.
2 changes: 2 additions & 0 deletions app/src/pages/ApiTokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useApiTokens } from '../features/tokens/hooks/useApiTokens';
import { useIsMobile } from '../features/toolbar/hooks/useIsMobile';
import { Button, TextField } from '@mui/material';
import TokenCard from '../features/tokens/components/TokenCard';
import { useGithubAuth } from '../features/toolbar/hooks/useGithubAuth';
import { useNavigate } from 'react-router-dom';

function ApiTokens() {
const [tokenName, setTokenName] = React.useState('');
Expand Down
4 changes: 2 additions & 2 deletions src/api/api_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ pub struct Token {
pub token: Option<String>,
}

impl From<models::Token> for Token {
fn from(token: models::Token) -> Self {
impl From<models::ApiToken> for Token {
fn from(token: models::ApiToken) -> Self {
Token {
id: token.id.to_string(),
name: token.friendly_name,
Expand Down
1 change: 1 addition & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod api_token;
pub mod auth;
pub mod publish;

use rocket::{
http::Status,
Expand Down
8 changes: 8 additions & 0 deletions src/api/publish.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use rocket::serde::Deserialize;

/// The publish request.
#[derive(Deserialize, Debug)]
pub struct PublishRequest {
pub name: String,
pub version: String,
}
41 changes: 33 additions & 8 deletions src/db/api_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ const TOKEN_LENGTH: usize = 32;
#[derive(Debug)]
pub struct PlainToken(String);

impl Default for PlainToken {
fn default() -> Self {
Self::new()
}
}

impl PlainToken {
pub fn hash(&self) -> Vec<u8> {
Sha256::digest(self.0.as_bytes()).as_slice().to_vec()
Expand All @@ -33,9 +39,15 @@ impl PlainToken {
}
}

impl Into<String> for PlainToken {
fn into(self) -> String {
self.0
impl From<String> for PlainToken {
fn from(s: String) -> Self {
Self(s)
}
}

impl From<PlainToken> for String {
fn from(val: PlainToken) -> Self {
val.0
}
}

Expand All @@ -45,11 +57,11 @@ impl DbConn {
&mut self,
user_id: Uuid,
friendly_name: String,
) -> Result<(models::Token, PlainToken), DatabaseError> {
) -> Result<(models::ApiToken, PlainToken), DatabaseError> {
let plain_token = PlainToken::new();
let token = plain_token.hash();

let new_token = models::NewToken {
let new_token = models::NewApiToken {
user_id,
friendly_name,
token,
Expand All @@ -59,7 +71,7 @@ impl DbConn {
// Insert new session
let saved_token = diesel::insert_into(schema::api_tokens::table)
.values(&new_token)
.returning(models::Token::as_returning())
.returning(models::ApiToken::as_returning())
.get_result(self.inner())
.map_err(|_| DatabaseError::InsertTokenFailed(user_id.to_string()))?;

Expand All @@ -85,11 +97,24 @@ impl DbConn {
pub fn get_tokens_for_user(
&mut self,
user_id: Uuid,
) -> Result<Vec<models::Token>, DatabaseError> {
) -> Result<Vec<models::ApiToken>, DatabaseError> {
schema::api_tokens::table
.filter(schema::api_tokens::user_id.eq(user_id))
.select(models::Token::as_returning())
.select(models::ApiToken::as_returning())
.load(self.inner())
.map_err(|_| DatabaseError::NotFound(user_id.to_string()))
}

/// Fetch an API token given the plaintext token.
pub fn get_token(
&mut self,
plain_token: PlainToken,
) -> Result<models::ApiToken, DatabaseError> {
let hashed = plain_token.hash();
schema::api_tokens::table
.filter(schema::api_tokens::token.eq(hashed))
.select(models::ApiToken::as_returning())
.first::<models::ApiToken>(self.inner())
.map_err(|_| DatabaseError::NotFound("API Token".to_string()))
}
}
2 changes: 1 addition & 1 deletion src/db/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod api_token;
pub mod api_token;
pub mod error;
mod user_session;

Expand Down
18 changes: 14 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@
extern crate rocket;

use forc_pub::api::api_token::{CreateTokenRequest, CreateTokenResponse, Token, TokensResponse};
use forc_pub::api::publish::PublishRequest;
use forc_pub::api::{
auth::{LoginRequest, LoginResponse, UserResponse},
ApiResult, EmptyResponse,
};
use forc_pub::middleware::cors::Cors;

use forc_pub::db::Database;
use forc_pub::github::handle_login;
use forc_pub::middleware::cors::Cors;
use forc_pub::middleware::session_auth::{SessionAuth, SESSION_COOKIE_NAME};

use forc_pub::middleware::token_auth::TokenAuth;
use rocket::http::{Cookie, CookieJar};

use rocket::{serde::json::Json, State};

#[derive(Default)]
Expand Down Expand Up @@ -86,6 +85,16 @@ fn tokens(db: &State<Database>, auth: SessionAuth) -> ApiResult<TokensResponse>
}))
}

#[post("/publish", data = "<request>")]
fn publish(request: Json<PublishRequest>, auth: TokenAuth) -> ApiResult<EmptyResponse> {
println!(
"Publishing: {:?} for token: {:?}",
request, auth.token.friendly_name
);

Ok(Json(EmptyResponse))
}

/// Catches all OPTION requests in order to get the CORS related Fairing triggered.
#[options("/<_..>")]
fn all_options() {
Expand Down Expand Up @@ -118,6 +127,7 @@ fn rocket() -> _ {
user,
new_token,
delete_token,
publish,
tokens,
all_options,
health
Expand Down
1 change: 1 addition & 0 deletions src/middleware/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod cors;
pub mod session_auth;
pub mod token_auth;
52 changes: 52 additions & 0 deletions src/middleware/token_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::db::api_token::PlainToken;
use crate::db::Database;
use crate::models;
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use rocket::Request;



pub const SESSION_COOKIE_NAME: &str = "session";

pub struct TokenAuth {
pub token: models::ApiToken,
}

#[derive(Debug)]
pub enum TokenAuthError {
Missing,
Invalid,
DatabaseConnection,
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for TokenAuth {
type Error = TokenAuthError;

async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
// TODO: use fairing for db connection?
// let db = try_outcome!(request.guard::<Database>().await);

let mut db = match request.rocket().state::<Database>() {
Some(db) => db.conn(),
None => {
return Outcome::Failure((
Status::InternalServerError,
TokenAuthError::DatabaseConnection,
))
}
};

if let Some(auth_header) = request.headers().get_one("Authorization") {
if auth_header.starts_with("Bearer ") {
let token = auth_header.trim_start_matches("Bearer ");
if let Ok(token) = db.get_token(PlainToken::from(token.to_string())) {
return Outcome::Success(TokenAuth { token });
}
}
return Outcome::Failure((Status::Unauthorized, TokenAuthError::Invalid));
}
return Outcome::Failure((Status::Unauthorized, TokenAuthError::Missing));
}
}
6 changes: 3 additions & 3 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ pub struct NewSession {
pub expires_at: SystemTime,
}

#[derive(Queryable, Selectable, Debug)]
#[derive(Queryable, Selectable, Debug, PartialEq, Eq)]
#[diesel(table_name = crate::schema::api_tokens)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Token {
pub struct ApiToken {
pub id: Uuid,
pub user_id: Uuid,
pub friendly_name: String,
Expand All @@ -57,7 +57,7 @@ pub struct Token {

#[derive(Insertable)]
#[diesel(table_name = crate::schema::api_tokens)]
pub struct NewToken {
pub struct NewApiToken {
pub user_id: Uuid,
pub friendly_name: String,
pub token: Vec<u8>,
Expand Down
28 changes: 20 additions & 8 deletions tests/db_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,31 +83,43 @@ fn test_user_sessions() {
fn test_api_tokens() {
let db = &mut Database::default().conn();

let session = db.insert_user_session(&mock_user_1(), 1000).expect("result is ok");
let session = db
.insert_user_session(&mock_user_1(), 1000)
.expect("result is ok");
let user = db.get_user_for_session(session.id).expect("result is ok");

// Insert tokens
let (token1, plain_token1) = db.new_token(user.id, TEST_TOKEN_NAME_1.into()).expect("result is ok");
let (token2, plain_token2) = db.new_token(user.id, TEST_TOKEN_NAME_2.into()).expect("result is ok");
let (token1, plain_token1) = db
.new_token(user.id, TEST_TOKEN_NAME_1.into())
.expect("result is ok");
let (token2, plain_token2) = db
.new_token(user.id, TEST_TOKEN_NAME_2.into())
.expect("result is ok");

assert_eq!(token1.friendly_name, TEST_TOKEN_NAME_1);
assert_eq!(token1.expires_at, None);
assert_eq!(token2.friendly_name, TEST_TOKEN_NAME_2);
assert_eq!(token2.expires_at, None);

// Test token hashing
assert_eq!(token1, db.get_token(plain_token1).expect("test token 1"));
assert_eq!(token2, db.get_token(plain_token2).expect("test token 2"));

// Get tokens
let tokens = db.get_tokens_for_user(user.id).expect("result is ok");
assert_eq!(tokens.len(), 2);

// Delete tokens
let _ = db.delete_token(user.id, token1.id.into()).expect("result is ok");
db
.delete_token(user.id, token1.id.into())
.expect("result is ok");
let tokens = db.get_tokens_for_user(user.id).expect("result is ok");
assert_eq!(tokens.len(), 1);
let _ = db.delete_token(user.id, token2.id.into()).expect("result is ok");
db
.delete_token(user.id, token2.id.into())
.expect("result is ok");
let tokens = db.get_tokens_for_user(user.id).expect("result is ok");
assert_eq!(tokens.len(), 0);

// TODO: test validating a plain token

clear_tables(db);
}
}

0 comments on commit 45384af

Please sign in to comment.