ezidam: request guards: jwt admin, jwt user, verify jwt

This commit is contained in:
Philippe Loctaux 2023-03-19 00:25:35 +01:00
parent 009b8664fd
commit c9ef821d2b
10 changed files with 219 additions and 7 deletions

View file

@ -1,7 +1,9 @@
mod completed_setup; mod completed_setup;
mod jwt;
mod need_setup; mod need_setup;
mod refresh_token; mod refresh_token;
pub use self::jwt::*;
pub use completed_setup::CompletedSetup; pub use completed_setup::CompletedSetup;
pub use need_setup::NeedSetup; pub use need_setup::NeedSetup;
pub use refresh_token::RefreshToken; pub use refresh_token::RefreshToken;

View file

@ -0,0 +1,126 @@
use crate::database::Database;
use jwt::database::Key;
use jwt::{JwtClaims, PrivateKey};
use rocket::http::Status;
use rocket::request::Outcome;
use rocket::tokio::task;
use rocket::Request;
mod admin;
mod user;
pub use admin::JwtAdmin;
use id::KeyID;
pub use user::JwtUser;
#[derive(Debug)]
pub enum Error {
GetDatabase,
Keys(jwt::Error),
JwtParsing(jwt::Error),
NoSigningKey,
NonExistentKey(String),
RevokedKey(KeyID),
ImportKey(jwt::Error),
JwtValidation(jwt::Error),
BlockingTask(String),
}
pub async fn get_jwt<T>(
request: &Request<'_>,
get_admin: Option<bool>,
) -> Result<Option<JwtClaims>, Outcome<T, Error>> {
// Get jwt
let jwt = match request
.cookies()
.get("access_token")
.map(|cookie| cookie.value())
{
Some(jwt) => jwt,
None => {
return Err(Outcome::Forward(()));
}
};
// Get database
let db = match request.guard::<&Database>().await {
Outcome::Success(database) => database,
Outcome::Failure(e) => return Err(Outcome::Failure((e.0, Error::GetDatabase))),
Outcome::Forward(f) => return Err(Outcome::Forward(f)),
};
// Get keys
let keys = match Key::get_all(&**db, Some(false)).await {
Ok(keys) => keys,
Err(e) => {
return Err(Outcome::Failure((
Status::InternalServerError,
Error::Keys(e),
)))
}
};
let jwt = jwt.to_string();
match task::spawn_blocking(move || -> Result<Option<JwtClaims>, Error> {
// Parse jwt
let parsed_jwt = jwt::parse(&jwt).map_err(Error::JwtParsing)?;
// Get key id
let jwk_id = parsed_jwt
.header()
.key_id
.as_deref()
.ok_or(Error::NoSigningKey)?;
// Get key
let key = keys
.iter()
.find(|&key| key.key_id().as_ref() == jwk_id)
.ok_or_else(|| Error::NonExistentKey(jwk_id.into()))?;
// If key has been revoked
if key.is_revoked() {
return Err(Error::RevokedKey(key.key_id().to_owned()));
}
// Import private key
let private_key =
PrivateKey::from_der(key.private_der(), key.key_id()).map_err(Error::ImportKey)?;
// Validate jwt and get claims
let jwt_claims = private_key
.validate_jwt_extract_claims(&parsed_jwt)
.map_err(Error::JwtValidation)?;
// Is specific kind of user required?
match get_admin {
// Yes, need to get specific kind of user
Some(get_admin) => {
if jwt_claims.is_admin == get_admin {
Ok(Some(jwt_claims))
} else {
Ok(None)
}
}
// No, any user is good
None => Ok(Some(jwt_claims)),
}
})
.await
{
Ok(result) => match result {
Ok(claims) => {
// Return jwt claims
Ok(claims)
}
Err(e) => Err(Outcome::Failure((Status::InternalServerError, e))),
},
Err(e) => {
// Failed to run blocking task
Err(Outcome::Failure((
Status::InternalServerError,
Error::BlockingTask(e.to_string()),
)))
}
}
}

View file

@ -0,0 +1,20 @@
use jwt::JwtClaims;
use rocket::request::{FromRequest, Outcome};
use rocket::Request;
pub struct JwtAdmin(pub JwtClaims);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for JwtAdmin {
type Error = super::Error;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match super::get_jwt(request, Some(true)).await {
Ok(jwt_claims) => match jwt_claims {
Some(jwt_claims) => Outcome::Success(JwtAdmin(jwt_claims)),
None => Outcome::Forward(()),
},
Err(e) => return e,
}
}
}

View file

@ -0,0 +1,20 @@
use jwt::JwtClaims;
use rocket::request::{FromRequest, Outcome};
use rocket::Request;
pub struct JwtUser(pub JwtClaims);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for JwtUser {
type Error = super::Error;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match super::get_jwt(request, None).await {
Ok(jwt_claims) => match jwt_claims {
Some(jwt_claims) => Outcome::Success(JwtUser(jwt_claims)),
None => Outcome::Forward(()),
},
Err(e) => return e,
}
}
}

View file

@ -5,7 +5,7 @@ pub struct RefreshToken(pub String);
#[rocket::async_trait] #[rocket::async_trait]
impl<'r> FromRequest<'r> for RefreshToken { impl<'r> FromRequest<'r> for RefreshToken {
type Error = (); type Error = std::convert::Infallible;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.cookies().get("refresh_token") { match request.cookies().get("refresh_token") {

View file

@ -2,8 +2,8 @@ use authorize::*;
use redirect::*; use redirect::*;
use rocket::{routes, Route}; use rocket::{routes, Route};
mod authorize; pub mod authorize;
mod redirect; pub mod redirect;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![

View file

@ -5,7 +5,15 @@ use settings::Settings;
use users::User; use users::User;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![logo, avatar, homepage, redirect_to_setup, logout] routes![
logo,
avatar,
homepage,
homepage_user,
homepage_redirect,
redirect_to_setup,
logout
]
} }
#[get("/logo")] #[get("/logo")]
@ -44,6 +52,7 @@ mod test {
#[get("/avatar/<user_id>?<size>")] #[get("/avatar/<user_id>?<size>")]
async fn avatar( async fn avatar(
mut db: Connection<Database>, mut db: Connection<Database>,
_user: JwtUser,
user_id: RocketUserID, user_id: RocketUserID,
size: Option<u32>, size: Option<u32>,
) -> Result<FileFromBytes> { ) -> Result<FileFromBytes> {
@ -87,12 +96,26 @@ async fn redirect_to_setup(_setup: NeedSetup) -> Redirect {
} }
#[get("/", rank = 2)] #[get("/", rank = 2)]
async fn homepage() -> Page { async fn homepage(admin: JwtAdmin) -> Page {
println!("{:?}", admin.0);
Page::Homepage(content::Homepage { Page::Homepage(content::Homepage {
abc: "string".to_string(), abc: "admin".to_string(),
}) })
} }
#[get("/", rank = 3)]
async fn homepage_user(user: JwtUser) -> Page {
println!("{:?}", user.0);
Page::Homepage(content::Homepage {
abc: "user".to_string(),
})
}
#[get("/", rank = 4)]
async fn homepage_redirect() -> Redirect {
Redirect::to(uri!(super::oauth::authorize::authorize_ezidam))
}
#[post("/logout")] #[post("/logout")]
async fn logout( async fn logout(
mut db: Connection<Database>, mut db: Connection<Database>,

View file

@ -23,4 +23,7 @@ pub enum Error {
#[error("Failed to create JWT: `{0}`")] #[error("Failed to create JWT: `{0}`")]
JwtCreation(#[from] jwt_compact::CreationError), JwtCreation(#[from] jwt_compact::CreationError),
#[error("Failed to validate JWT: `{0}`")]
JwtValidation(#[from] jwt_compact::ValidationError),
} }

View file

@ -1,7 +1,7 @@
use crate::{Error, JwtClaims}; use crate::{Error, JwtClaims};
use id::KeyID; use id::KeyID;
use jwt_compact::alg::{Rsa, RsaPrivateKey, StrongKey}; use jwt_compact::alg::{Rsa, RsaPrivateKey, StrongKey};
use jwt_compact::{AlgorithmExt, Claims, Header}; use jwt_compact::{AlgorithmExt, Claims, Header, TimeOptions, Token, UntrustedToken};
use rsa::pkcs8::der::zeroize::Zeroizing; use rsa::pkcs8::der::zeroize::Zeroizing;
use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey}; use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey};
@ -40,6 +40,23 @@ impl PrivateKey {
) -> Result<String, Error> { ) -> Result<String, Error> {
Ok(Rsa::ps256().token(header, &claims, &self.key)?) Ok(Rsa::ps256().token(header, &claims, &self.key)?)
} }
pub fn validate_jwt_extract_claims(&self, token: &UntrustedToken) -> Result<JwtClaims, Error> {
// Verify signature
let token: Token<JwtClaims> = Rsa::ps256()
.validate_integrity(token, &self.key)
.map_err(Error::JwtValidation)?;
// Validate additional conditions
let time_options = TimeOptions::default();
token
.claims()
.validate_expiration(&time_options)
.map_err(Error::JwtValidation)?;
// Return claims
Ok(token.claims().custom.clone())
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -12,3 +12,4 @@ pub use claims::JwtClaims;
pub use error::Error; pub use error::Error;
pub use key::generate; pub use key::generate;
pub use key::{PrivateKey, PublicKey}; pub use key::{PrivateKey, PublicKey};
pub use token::parse;