ezidam: request guards: jwt admin, jwt user, verify jwt
This commit is contained in:
parent
009b8664fd
commit
c9ef821d2b
10 changed files with 219 additions and 7 deletions
|
|
@ -1,7 +1,9 @@
|
|||
mod completed_setup;
|
||||
mod jwt;
|
||||
mod need_setup;
|
||||
mod refresh_token;
|
||||
|
||||
pub use self::jwt::*;
|
||||
pub use completed_setup::CompletedSetup;
|
||||
pub use need_setup::NeedSetup;
|
||||
pub use refresh_token::RefreshToken;
|
||||
|
|
|
|||
126
crates/ezidam/src/guards/jwt.rs
Normal file
126
crates/ezidam/src/guards/jwt.rs
Normal 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()),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
20
crates/ezidam/src/guards/jwt/admin.rs
Normal file
20
crates/ezidam/src/guards/jwt/admin.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
20
crates/ezidam/src/guards/jwt/user.rs
Normal file
20
crates/ezidam/src/guards/jwt/user.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ pub struct RefreshToken(pub String);
|
|||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for RefreshToken {
|
||||
type Error = ();
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
match request.cookies().get("refresh_token") {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ use authorize::*;
|
|||
use redirect::*;
|
||||
use rocket::{routes, Route};
|
||||
|
||||
mod authorize;
|
||||
mod redirect;
|
||||
pub mod authorize;
|
||||
pub mod redirect;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
|
|
|
|||
|
|
@ -5,7 +5,15 @@ use settings::Settings;
|
|||
use users::User;
|
||||
|
||||
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")]
|
||||
|
|
@ -44,6 +52,7 @@ mod test {
|
|||
#[get("/avatar/<user_id>?<size>")]
|
||||
async fn avatar(
|
||||
mut db: Connection<Database>,
|
||||
_user: JwtUser,
|
||||
user_id: RocketUserID,
|
||||
size: Option<u32>,
|
||||
) -> Result<FileFromBytes> {
|
||||
|
|
@ -87,12 +96,26 @@ async fn redirect_to_setup(_setup: NeedSetup) -> Redirect {
|
|||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
async fn homepage() -> Page {
|
||||
async fn homepage(admin: JwtAdmin) -> Page {
|
||||
println!("{:?}", admin.0);
|
||||
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")]
|
||||
async fn logout(
|
||||
mut db: Connection<Database>,
|
||||
|
|
|
|||
|
|
@ -23,4 +23,7 @@ pub enum Error {
|
|||
|
||||
#[error("Failed to create JWT: `{0}`")]
|
||||
JwtCreation(#[from] jwt_compact::CreationError),
|
||||
|
||||
#[error("Failed to validate JWT: `{0}`")]
|
||||
JwtValidation(#[from] jwt_compact::ValidationError),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{Error, JwtClaims};
|
||||
use id::KeyID;
|
||||
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::{DecodePrivateKey, EncodePrivateKey};
|
||||
|
||||
|
|
@ -40,6 +40,23 @@ impl PrivateKey {
|
|||
) -> Result<String, Error> {
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@ pub use claims::JwtClaims;
|
|||
pub use error::Error;
|
||||
pub use key::generate;
|
||||
pub use key::{PrivateKey, PublicKey};
|
||||
pub use token::parse;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue