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 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;
|
||||||
|
|
|
||||||
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]
|
#[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") {
|
||||||
|
|
|
||||||
|
|
@ -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![
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue