// // JWT Handling // use chrono::{Duration, Utc}; use num_traits::FromPrimitive; use once_cell::sync::Lazy; use jsonwebtoken::{self, Algorithm, DecodingKey, EncodingKey, Header}; use serde::de::DeserializeOwned; use serde::ser::Serialize; use crate::{ error::{Error, MapResult}, util::read_file, CONFIG, }; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; pub static DEFAULT_VALIDITY: Lazy = Lazy::new(|| Duration::hours(2)); static JWT_HEADER: Lazy
= Lazy::new(|| Header::new(JWT_ALGORITHM)); pub static JWT_LOGIN_ISSUER: Lazy = Lazy::new(|| format!("{}|login", CONFIG.domain_origin())); static JWT_INVITE_ISSUER: Lazy = Lazy::new(|| format!("{}|invite", CONFIG.domain_origin())); static JWT_DELETE_ISSUER: Lazy = Lazy::new(|| format!("{}|delete", CONFIG.domain_origin())); static JWT_VERIFYEMAIL_ISSUER: Lazy = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin())); static JWT_ADMIN_ISSUER: Lazy = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin())); static PRIVATE_RSA_KEY: Lazy> = Lazy::new(|| match read_file(&CONFIG.private_rsa_key()) { Ok(key) => key, Err(e) => panic!("Error loading private RSA Key.\n Error: {}", e), }); static PUBLIC_RSA_KEY: Lazy> = Lazy::new(|| match read_file(&CONFIG.public_rsa_key()) { Ok(key) => key, Err(e) => panic!("Error loading public RSA Key.\n Error: {}", e), }); pub fn encode_jwt(claims: &T) -> String { match jsonwebtoken::encode(&JWT_HEADER, claims, &EncodingKey::from_rsa_der(&PRIVATE_RSA_KEY)) { Ok(token) => token, Err(e) => panic!("Error encoding jwt {}", e), } } fn decode_jwt(token: &str, issuer: String) -> Result { let validation = jsonwebtoken::Validation { leeway: 30, // 30 seconds validate_exp: true, validate_nbf: true, aud: None, iss: Some(issuer), sub: None, algorithms: vec![JWT_ALGORITHM], }; let token = token.replace(char::is_whitespace, ""); jsonwebtoken::decode(&token, &DecodingKey::from_rsa_der(&PUBLIC_RSA_KEY), &validation) .map(|d| d.claims) .map_res("Error decoding JWT") } pub fn decode_login(token: &str) -> Result { decode_jwt(token, JWT_LOGIN_ISSUER.to_string()) } pub fn decode_invite(token: &str) -> Result { decode_jwt(token, JWT_INVITE_ISSUER.to_string()) } pub fn decode_delete(token: &str) -> Result { decode_jwt(token, JWT_DELETE_ISSUER.to_string()) } pub fn decode_verify_email(token: &str) -> Result { decode_jwt(token, JWT_VERIFYEMAIL_ISSUER.to_string()) } pub fn decode_admin(token: &str) -> Result { decode_jwt(token, JWT_ADMIN_ISSUER.to_string()) } #[derive(Debug, Serialize, Deserialize)] pub struct LoginJWTClaims { // Not before pub nbf: i64, // Expiration time pub exp: i64, // Issuer pub iss: String, // Subject pub sub: String, pub premium: bool, pub name: String, pub email: String, pub email_verified: bool, pub orgowner: Vec, pub orgadmin: Vec, pub orguser: Vec, pub orgmanager: Vec, // user security_stamp pub sstamp: String, // device uuid pub device: String, // [ "api", "offline_access" ] pub scope: Vec, // [ "Application" ] pub amr: Vec, } #[derive(Debug, Serialize, Deserialize)] pub struct InviteJWTClaims { // Not before pub nbf: i64, // Expiration time pub exp: i64, // Issuer pub iss: String, // Subject pub sub: String, pub email: String, pub org_id: Option, pub user_org_id: Option, pub invited_by_email: Option, } pub fn generate_invite_claims( uuid: String, email: String, org_id: Option, user_org_id: Option, invited_by_email: Option, ) -> InviteJWTClaims { let time_now = Utc::now().naive_utc(); InviteJWTClaims { nbf: time_now.timestamp(), exp: (time_now + Duration::days(5)).timestamp(), iss: JWT_INVITE_ISSUER.to_string(), sub: uuid, email, org_id, user_org_id, invited_by_email, } } #[derive(Debug, Serialize, Deserialize)] pub struct DeleteJWTClaims { // Not before pub nbf: i64, // Expiration time pub exp: i64, // Issuer pub iss: String, // Subject pub sub: String, } pub fn generate_delete_claims(uuid: String) -> DeleteJWTClaims { let time_now = Utc::now().naive_utc(); DeleteJWTClaims { nbf: time_now.timestamp(), exp: (time_now + Duration::days(5)).timestamp(), iss: JWT_DELETE_ISSUER.to_string(), sub: uuid, } } #[derive(Debug, Serialize, Deserialize)] pub struct VerifyEmailJWTClaims { // Not before pub nbf: i64, // Expiration time pub exp: i64, // Issuer pub iss: String, // Subject pub sub: String, } pub fn generate_verify_email_claims(uuid: String) -> DeleteJWTClaims { let time_now = Utc::now().naive_utc(); DeleteJWTClaims { nbf: time_now.timestamp(), exp: (time_now + Duration::days(5)).timestamp(), iss: JWT_VERIFYEMAIL_ISSUER.to_string(), sub: uuid, } } #[derive(Debug, Serialize, Deserialize)] pub struct AdminJWTClaims { // Not before pub nbf: i64, // Expiration time pub exp: i64, // Issuer pub iss: String, // Subject pub sub: String, } pub fn generate_admin_claims() -> AdminJWTClaims { let time_now = Utc::now().naive_utc(); AdminJWTClaims { nbf: time_now.timestamp(), exp: (time_now + Duration::minutes(20)).timestamp(), iss: JWT_ADMIN_ISSUER.to_string(), sub: "admin_panel".to_string(), } } // // Bearer token authentication // use rocket::request::{FromRequest, Outcome, Request}; use crate::db::{ models::{CollectionUser, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException}, DbConn, }; pub struct Headers { pub host: String, pub device: Device, pub user: User, } impl<'a, 'r> FromRequest<'a, 'r> for Headers { type Error = &'static str; fn from_request(request: &'a Request<'r>) -> Outcome { let headers = request.headers(); // Get host let host = if CONFIG.domain_set() { CONFIG.domain() } else if let Some(referer) = headers.get_one("Referer") { referer.to_string() } else { // Try to guess from the headers use std::env; let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") { proto } else if env::var("ROCKET_TLS").is_ok() { "https" } else { "http" }; let host = if let Some(host) = headers.get_one("X-Forwarded-Host") { host } else if let Some(host) = headers.get_one("Host") { host } else { "" }; format!("{}://{}", protocol, host) }; // Get access_token let access_token: &str = match headers.get_one("Authorization") { Some(a) => match a.rsplit("Bearer ").next() { Some(split) => split, None => err_handler!("No access token provided"), }, None => err_handler!("No access token provided"), }; // Check JWT token is valid and get device and user from it let claims = match decode_login(access_token) { Ok(claims) => claims, Err(_) => err_handler!("Invalid claim"), }; let device_uuid = claims.device; let user_uuid = claims.sub; let conn = match request.guard::() { Outcome::Success(conn) => conn, _ => err_handler!("Error getting DB"), }; let device = match Device::find_by_uuid(&device_uuid, &conn) { Some(device) => device, None => err_handler!("Invalid device id"), }; let user = match User::find_by_uuid(&user_uuid, &conn) { Some(user) => user, None => err_handler!("Device has no user associated"), }; if user.security_stamp != claims.sstamp { if let Some(stamp_exception) = user .stamp_exception .as_deref() .and_then(|s| serde_json::from_str::(s).ok()) { let current_route = match request.route().and_then(|r| r.name) { Some(name) => name, _ => err_handler!("Error getting current route for stamp exception"), }; // Check if both match, if not this route is not allowed with the current security stamp. if stamp_exception.route != current_route { err_handler!("Invalid security stamp: Current route and exception route do not match") } else if stamp_exception.security_stamp != claims.sstamp { err_handler!("Invalid security stamp for matched stamp exception") } } else { err_handler!("Invalid security stamp") } } Outcome::Success(Headers { host, device, user }) } } pub struct OrgHeaders { pub host: String, pub device: Device, pub user: User, pub org_user_type: UserOrgType, pub org_user: UserOrganization, pub org_id: String, } // org_id is usually the second param ("/organizations/") // But there are cases where it is located in a query value. // First check the param, if this is not a valid uuid, we will try the query value. fn get_org_id(request: &Request) -> Option { if let Some(Ok(org_id)) = request.get_param::(1) { if uuid::Uuid::parse_str(&org_id).is_ok() { return Some(org_id); } } if let Some(Ok(org_id)) = request.get_query_value::("organizationId") { if uuid::Uuid::parse_str(&org_id).is_ok() { return Some(org_id); } } None } impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders { type Error = &'static str; fn from_request(request: &'a Request<'r>) -> Outcome { match request.guard::() { Outcome::Forward(_) => Outcome::Forward(()), Outcome::Failure(f) => Outcome::Failure(f), Outcome::Success(headers) => { match get_org_id(request) { Some(org_id) => { let conn = match request.guard::() { Outcome::Success(conn) => conn, _ => err_handler!("Error getting DB"), }; let user = headers.user; let org_user = match UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn) { Some(user) => { if user.status == UserOrgStatus::Confirmed as i32 { user } else { err_handler!("The current user isn't confirmed member of the organization") } } None => err_handler!("The current user isn't member of the organization"), }; Outcome::Success(Self { host: headers.host, device: headers.device, user, org_user_type: { if let Some(org_usr_type) = UserOrgType::from_i32(org_user.atype) { org_usr_type } else { // This should only happen if the DB is corrupted err_handler!("Unknown user type in the database") } }, org_user, org_id, }) } _ => err_handler!("Error getting the organization id"), } } } } } pub struct AdminHeaders { pub host: String, pub device: Device, pub user: User, pub org_user_type: UserOrgType, } impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders { type Error = &'static str; fn from_request(request: &'a Request<'r>) -> Outcome { match request.guard::() { Outcome::Forward(_) => Outcome::Forward(()), Outcome::Failure(f) => Outcome::Failure(f), Outcome::Success(headers) => { if headers.org_user_type >= UserOrgType::Admin { Outcome::Success(Self { host: headers.host, device: headers.device, user: headers.user, org_user_type: headers.org_user_type, }) } else { err_handler!("You need to be Admin or Owner to call this endpoint") } } } } } impl Into for AdminHeaders { fn into(self) -> Headers { Headers { host: self.host, device: self.device, user: self.user, } } } // col_id is usually the forth param ("/organizations//collections/") // But there cloud be cases where it is located in a query value. // First check the param, if this is not a valid uuid, we will try the query value. fn get_col_id(request: &Request) -> Option { if let Some(Ok(col_id)) = request.get_param::(3) { if uuid::Uuid::parse_str(&col_id).is_ok() { return Some(col_id); } } if let Some(Ok(col_id)) = request.get_query_value::("collectionId") { if uuid::Uuid::parse_str(&col_id).is_ok() { return Some(col_id); } } None } /// The ManagerHeaders are used to check if you are at least a Manager /// and have access to the specific collection provided via the /collections/collectionId. /// This does strict checking on the collection_id, ManagerHeadersLoose does not. pub struct ManagerHeaders { pub host: String, pub device: Device, pub user: User, pub org_user_type: UserOrgType, } impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeaders { type Error = &'static str; fn from_request(request: &'a Request<'r>) -> Outcome { match request.guard::() { Outcome::Forward(_) => Outcome::Forward(()), Outcome::Failure(f) => Outcome::Failure(f), Outcome::Success(headers) => { if headers.org_user_type >= UserOrgType::Manager { match get_col_id(request) { Some(col_id) => { let conn = match request.guard::() { Outcome::Success(conn) => conn, _ => err_handler!("Error getting DB"), }; if !headers.org_user.access_all { match CollectionUser::find_by_collection_and_user(&col_id, &headers.org_user.user_uuid, &conn) { Some(_) => (), None => err_handler!("The current user isn't a manager for this collection"), } } } _ => err_handler!("Error getting the collection id"), } Outcome::Success(Self { host: headers.host, device: headers.device, user: headers.user, org_user_type: headers.org_user_type, }) } else { err_handler!("You need to be a Manager, Admin or Owner to call this endpoint") } } } } } impl Into for ManagerHeaders { fn into(self) -> Headers { Headers { host: self.host, device: self.device, user: self.user, } } } /// The ManagerHeadersLoose is used when you at least need to be a Manager, /// but there is no collection_id sent with the request (either in the path or as form data). pub struct ManagerHeadersLoose { pub host: String, pub device: Device, pub user: User, pub org_user_type: UserOrgType, } impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeadersLoose { type Error = &'static str; fn from_request(request: &'a Request<'r>) -> Outcome { match request.guard::() { Outcome::Forward(_) => Outcome::Forward(()), Outcome::Failure(f) => Outcome::Failure(f), Outcome::Success(headers) => { if headers.org_user_type >= UserOrgType::Manager { Outcome::Success(Self { host: headers.host, device: headers.device, user: headers.user, org_user_type: headers.org_user_type, }) } else { err_handler!("You need to be a Manager, Admin or Owner to call this endpoint") } } } } } impl Into for ManagerHeadersLoose { fn into(self) -> Headers { Headers { host: self.host, device: self.device, user: self.user, } } } pub struct OwnerHeaders { pub host: String, pub device: Device, pub user: User, } impl<'a, 'r> FromRequest<'a, 'r> for OwnerHeaders { type Error = &'static str; fn from_request(request: &'a Request<'r>) -> Outcome { match request.guard::() { Outcome::Forward(_) => Outcome::Forward(()), Outcome::Failure(f) => Outcome::Failure(f), Outcome::Success(headers) => { if headers.org_user_type == UserOrgType::Owner { Outcome::Success(Self { host: headers.host, device: headers.device, user: headers.user, }) } else { err_handler!("You need to be Owner to call this endpoint") } } } } } // // Client IP address detection // use std::net::IpAddr; pub struct ClientIp { pub ip: IpAddr, } impl<'a, 'r> FromRequest<'a, 'r> for ClientIp { type Error = (); fn from_request(req: &'a Request<'r>) -> Outcome { let ip = if CONFIG._ip_header_enabled() { req.headers().get_one(&CONFIG.ip_header()).and_then(|ip| { match ip.find(',') { Some(idx) => &ip[..idx], None => ip, } .parse() .map_err(|_| warn!("'{}' header is malformed: {}", CONFIG.ip_header(), ip)) .ok() }) } else { None }; let ip = ip .or_else(|| req.remote().map(|r| r.ip())) .unwrap_or_else(|| "0.0.0.0".parse().unwrap()); Outcome::Success(ClientIp { ip }) } }