Merge pull request #11 from EETagent/jwt_auth

Jwt auth
This commit is contained in:
Vojtěch Jungmann 2022-10-26 15:11:06 +02:00 committed by GitHub
commit 4473241db0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 299 additions and 1 deletions

View file

@ -0,0 +1,33 @@
use rocket::http::Status;
use rocket::outcome::Outcome;
use rocket::request::{FromRequest, Request};
use portfolio_core::token::candidate_token::CandidateToken;
use portfolio_core::token::decode_candidate_token;
pub struct TokenRequest(CandidateToken);
impl TokenRequest {
pub fn to_token(self) -> CandidateToken {
self.0
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for TokenRequest {
type Error = Status;
async fn from_request(req: &'r Request<'_>) -> Outcome<TokenRequest, (Status, Status), ()> {
if let Some(auth) = req.headers().get_one("Authorization") {
let auth_string = auth.to_string();
if auth_string.starts_with("Bearer") {
let token = auth_string[6..auth_string.len()].trim();
let token_data = decode_candidate_token(token.to_string());
if token_data.is_ok() {
return Outcome::Success(TokenRequest(token_data.ok().unwrap().claims));
}
}
}
return Outcome::Failure((Status::Unauthorized, Status::Unauthorized));
}
}

1
api/src/guard/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod candidate_jwt;

View file

@ -1,5 +1,11 @@
#[macro_use]
extern crate rocket;
use guard::candidate_jwt::TokenRequest;
use portfolio_core::error::ServiceError;
use portfolio_core::services::candidate_service::CandidateService;
use requests::LoginRequest;
use rocket::http::Status;
use rocket::{Rocket, Build};
use rocket::serde::json::Json;
use rocket::fairing::{self, AdHoc};
@ -9,7 +15,11 @@ use portfolio_core::{Mutation};
use migration::{MigratorTrait};
use sea_orm_rocket::{Connection, Database};
mod pool;
mod guard;
mod requests;
use pool::Db;
pub use entity::candidate;
@ -17,6 +27,9 @@ pub use entity::candidate::Entity as Candidate;
use portfolio_core::crypto::random_8_char_string;
fn custom_err_from_service_err(service_err: ServiceError) -> Custom<String> {
Custom(Status::from_code(service_err.0.code).unwrap_or_default(), service_err.1.to_string())
}
#[post("/", data = "<post_form>")]
async fn create(conn: Connection<'_, Db>, post_form: Json<candidate::Model>) -> Result<String, Custom<String>> {
@ -32,6 +45,38 @@ async fn create(conn: Connection<'_, Db>, post_form: Json<candidate::Model>) ->
Ok(plain_text_password)
}
#[post("/login", data = "<login_form>")]
async fn login(conn: Connection<'_, Db>, login_form: Json<LoginRequest>) -> Result<String, Custom<String>> {
let db = conn.into_inner();
println!("{} {}", login_form.application_id, login_form.password);
let jwt = CandidateService::login(db,
login_form.application_id,
login_form.password.to_owned()).await;
if jwt.is_ok() {
return Ok(
jwt.ok().unwrap()
);
} else {
return Err(
custom_err_from_service_err(jwt.err().unwrap())
)
}
}
#[get("/whoami")]
async fn whoami(conn: Connection<'_, Db>, token_req: Result<TokenRequest, Status>) -> Result<String, Custom<String>> {
let db = conn.into_inner();
let token = token_req.ok().unwrap().to_token();
let user = CandidateService::authenticate_candidate(db, token).await;
match user {
Ok(user) => Ok(format!("{} {}", user.name.unwrap(), user.surname.unwrap())),
Err(e) => Err(custom_err_from_service_err(e)),
}
}
#[get("/hello")]
async fn hello() -> &'static str {
"Hello, world!"
@ -49,7 +94,7 @@ async fn start() -> Result<(), rocket::Error> {
.attach(Db::init())
.attach(AdHoc::try_on_ignite("Migrations", run_migrations))
//.mount("/", FileServer::from(relative!("/static")))
.mount("/", routes![create, hello])
.mount("/", routes![create, login, hello, whoami])
.register("/", catchers![])
.launch()
.await

9
api/src/requests.rs Normal file
View file

@ -0,0 +1,9 @@
use rocket::serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct LoginRequest {
pub application_id: i32,
pub password: String,
}

View file

@ -4,10 +4,13 @@ version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
portfolio-entity = { path = "../entity" }
rand = "0.8.5"
argon2 = "0.4.1"
chrono = "0.4.22"
jsonwebtoken = "8.1.1"
dotenv = "0.15.0"
[dependencies.sea-orm]
version = "^0.10.0"

18
core/src/error.rs Normal file
View file

@ -0,0 +1,18 @@
pub struct Status {
pub code: u16,
}
pub const INVALID_CREDENTIALS_ERROR: ServiceError = ServiceError(Status { code: 401 },
"Invalid credentials");
pub const JWT_ERROR: ServiceError = ServiceError(Status { code: 500 },
"Error while encoding JWT");
pub const USER_NOT_FOUND_ERROR: ServiceError = ServiceError(Status { code: 404 },
"User not found");
pub const DB_ERROR: ServiceError = ServiceError(Status { code: 500 },
"Database error");
pub const USER_NOT_FOUND_BY_JWT_ID: ServiceError = ServiceError(Status { code: 500 }, // User got somehow
"User not found, please contact technical support"); // Shouldn't ever happen
pub struct ServiceError<'a>(pub Status, pub &'a str);

View file

@ -1,6 +1,9 @@
mod mutation;
mod query;
pub mod crypto;
pub mod token;
pub mod services;
pub mod error;
pub use mutation::*;
pub use query::*;

View file

@ -0,0 +1,43 @@
use entity::candidate;
use sea_orm::DatabaseConnection;
use crate::{crypto, Query, token::{generate_candidate_token, candidate_token::CandidateToken}, error::{ServiceError, USER_NOT_FOUND_ERROR, INVALID_CREDENTIALS_ERROR, DB_ERROR, USER_NOT_FOUND_BY_JWT_ID}};
pub struct CandidateService;
impl CandidateService {
pub async fn login(db: &DatabaseConnection, id: i32, password: String) -> Result<String, ServiceError> {
let candidate = match Query::find_candidate_by_id(db, id).await {
Ok(candidate) => match candidate {
Some(candidate) => candidate,
None => return Err(USER_NOT_FOUND_ERROR)
},
Err(_) => {return Err(DB_ERROR)}
};
let valid = crypto::verify_password(&password,&candidate.code )
.expect("Invalid password");
if !valid {
return Err(INVALID_CREDENTIALS_ERROR)
}
let jwt = generate_candidate_token(candidate); // TODO better error handling
Ok(jwt)
}
pub async fn authenticate_candidate(db: &DatabaseConnection, token: CandidateToken) -> Result<candidate::Model, ServiceError> {
let candidate = match Query::find_candidate_by_id(db, token.application_id).await {
Ok(candidate) => match candidate {
Some(candidate) => candidate,
None => return Err(USER_NOT_FOUND_BY_JWT_ID)
},
Err(_) => {return Err(DB_ERROR)}
};
Ok(candidate)
}
}

1
core/src/services/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod candidate_service;

View file

@ -0,0 +1,9 @@
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminToken {
// issued at
pub iat: i64,
// expiration
pub exp: i64,
}

View file

@ -0,0 +1,26 @@
use chrono::Utc;
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct CandidateToken {
// issued at
pub iat: i64,
// expiration
pub exp: i64,
pub application_id: i32,
pub name: String,
pub surname: String,
}
impl CandidateToken {
pub fn generate(application_id: i32, name: String, surname: String) -> Self {
let now = Utc::now().timestamp();
CandidateToken {
iat: now,
exp: now + 60 * 60, // 1 hour for now
application_id,
name,
surname,
}
}
}

98
core/src/token/mod.rs Normal file
View file

@ -0,0 +1,98 @@
pub mod admin_token;
pub mod candidate_token;
use chrono::Utc;
use entity::{admin, candidate};
use jsonwebtoken::errors::Result;
use jsonwebtoken::TokenData;
use jsonwebtoken::{DecodingKey, EncodingKey};
use jsonwebtoken::{Header, Validation};
use admin_token::AdminToken;
use candidate_token::CandidateToken;
use serde::Deserialize;
const ONE_WEEK: i64 = 60 * 60 * 24 * 7;
pub fn generate_candidate_token(candidate: candidate::Model) -> String {
let now = Utc::now().timestamp();
let payload = CandidateToken {
iat: now,
exp: now + ONE_WEEK,
application_id: candidate.application,
name: candidate.name.unwrap_or_else(|| "".into()),
surname: candidate.surname.unwrap_or_else(|| "".into()),
};
jsonwebtoken::encode(
&Header::default(),
&payload,
&EncodingKey::from_secret(include_bytes!("secret.key")),
)
.unwrap()
}
pub fn generate_admin_token(_admin: admin::Model) -> String {
let now = Utc::now().timestamp();
let payload = AdminToken {
iat: now,
exp: now + ONE_WEEK,
};
jsonwebtoken::encode(
&Header::default(),
&payload,
&EncodingKey::from_secret(include_bytes!("secret.key")),
)
.unwrap()
}
pub fn decode_token<T: for<'a> Deserialize<'a>>(token: String) -> Result<TokenData<T>> {
jsonwebtoken::decode::<T>(
&token,
&DecodingKey::from_secret(include_bytes!("secret.key")),
&Validation::default(),
)
}
pub fn decode_candidate_token(token: String) -> Result<TokenData<CandidateToken>> {
decode_token(token)
}
pub fn decode_admin_token(token: String) -> Result<TokenData<AdminToken>> {
decode_token(token)
}
#[test]
fn test_encode_decode_token() {
let candidate_model = candidate::Model {
application: 101204,
code: "random_code".to_string(),
birth_surname: None,
birthplace: None,
birthdate: None,
address: None,
telephone: None,
citizenship: None,
sex: None,
study: None,
personal_identification_number: None,
personal_identification_number_hash: None,
public_key: "None".to_owned(),
private_key: "None".to_owned(),
created_at: Utc::now().naive_local(),
updated_at: Utc::now().naive_local(),
name: Some("Uplnej".to_string()),
surname: Some("Magor".to_string()),
email: Some("email.uchazece@centrum.cz".to_string()),
};
let jwt = generate_candidate_token(candidate_model.clone());
let decoded = decode_candidate_token(jwt).unwrap();
let token_claims = decoded.claims;
assert_eq!(candidate_model.name.unwrap(), token_claims.name);
assert_eq!(candidate_model.surname.unwrap(), token_claims.surname);
}

View file

@ -0,0 +1 @@
temp

8
entity/src/mod.rs Normal file
View file

@ -0,0 +1,8 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.3
pub mod prelude;
pub mod admin;
pub mod candidate;
pub mod parent;
pub mod session;