From e99115e174cb4ad0878a6b2c526d32ce4ed5e8e3 Mon Sep 17 00:00:00 2001 From: Philippe Loctaux Date: Sat, 18 Mar 2023 16:14:26 +0100 Subject: [PATCH] ezidam + jwt: get key, import private key, create jwt claims and sign them --- Cargo.lock | 1 + crates/apps/src/database.rs | 9 +++ .../apps/get_one_from_authorization_code.sql | 13 ++++ crates/database/sqlx-data.json | 60 +++++++++++++++++++ crates/database/src/tables/apps.rs | 14 +++++ crates/ezidam/src/error.rs | 2 +- crates/ezidam/src/routes/oauth/redirect.rs | 30 +++++++++- crates/jwt/Cargo.toml | 1 + crates/jwt/src/claims.rs | 58 ++++++++++++++++++ crates/jwt/src/database.rs | 4 ++ crates/jwt/src/error.rs | 3 + crates/jwt/src/key/private.rs | 17 +++++- crates/jwt/src/lib.rs | 4 +- crates/users/src/lib.rs | 6 ++ 14 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 crates/database/queries/apps/get_one_from_authorization_code.sql create mode 100644 crates/jwt/src/claims.rs diff --git a/Cargo.lock b/Cargo.lock index d84d121..ff15d66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1535,6 +1535,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "users", ] [[package]] diff --git a/crates/apps/src/database.rs b/crates/apps/src/database.rs index 62a1412..a832d12 100644 --- a/crates/apps/src/database.rs +++ b/crates/apps/src/database.rs @@ -58,4 +58,13 @@ impl App { ) -> Result, Error> { Ok(DatabaseApps::get_one_by_id(conn, id).await?.map(Self::from)) } + + pub async fn get_one_from_authorization_code( + conn: impl SqliteExecutor<'_>, + code: &str, + ) -> Result, Error> { + Ok(DatabaseApps::get_one_from_authorization_code(conn, code) + .await? + .map(Self::from)) + } } diff --git a/crates/database/queries/apps/get_one_from_authorization_code.sql b/crates/database/queries/apps/get_one_from_authorization_code.sql new file mode 100644 index 0000000..de09bce --- /dev/null +++ b/crates/database/queries/apps/get_one_from_authorization_code.sql @@ -0,0 +1,13 @@ +select a.id, + a.created_at as "created_at: DateTime", + a.updated_at as "updated_at: DateTime", + a.label, + a.redirect_uri, + a.secret, + a.is_confidential as "is_confidential: bool", + a.is_archived as "is_archived: bool" +from apps a + + inner join authorization_codes ac on a.id = ac.app + +where ac.code is ? diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json index 431b660..1e842c9 100644 --- a/crates/database/sqlx-data.json +++ b/crates/database/sqlx-data.json @@ -648,6 +648,66 @@ }, "query": "select id,\n created_at as \"created_at: DateTime\",\n updated_at as \"updated_at: DateTime\",\n label,\n redirect_uri,\n secret,\n is_confidential as \"is_confidential: bool\",\n is_archived as \"is_archived: bool\"\nfrom apps\n\nwhere id is (?)\n" }, + "eaf0744f65a1de803fa8cc21b67bad4bdf22760d431265cf97b911e6456b2fd8": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "updated_at: DateTime", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "label", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "redirect_uri", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "secret", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "is_confidential: bool", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "is_archived: bool", + "ordinal": 7, + "type_info": "Int64" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "select a.id,\n a.created_at as \"created_at: DateTime\",\n a.updated_at as \"updated_at: DateTime\",\n a.label,\n a.redirect_uri,\n a.secret,\n a.is_confidential as \"is_confidential: bool\",\n a.is_archived as \"is_archived: bool\"\nfrom apps a\n\n inner join authorization_codes ac on a.id = ac.app\n\nwhere ac.code is ?\n" + }, "eb1a0153c88b0b2744ed1b71df04a91a129a0173fbbc3e2536f52d41e8dc99c4": { "describe": { "columns": [ diff --git a/crates/database/src/tables/apps.rs b/crates/database/src/tables/apps.rs index 2e81675..a76ac5b 100644 --- a/crates/database/src/tables/apps.rs +++ b/crates/database/src/tables/apps.rs @@ -59,4 +59,18 @@ impl Apps { .await .map_err(handle_error) } + + pub async fn get_one_from_authorization_code( + conn: impl SqliteExecutor<'_>, + code: &str, + ) -> Result, Error> { + sqlx::query_file_as!( + Self, + "queries/apps/get_one_from_authorization_code.sql", + code + ) + .fetch_optional(conn) + .await + .map_err(handle_error) + } } diff --git a/crates/ezidam/src/error.rs b/crates/ezidam/src/error.rs index 35f1290..a7944e1 100644 --- a/crates/ezidam/src/error.rs +++ b/crates/ezidam/src/error.rs @@ -36,7 +36,7 @@ impl Error { } } - pub fn internal_server_error(error: E) -> Self { + pub fn internal_server_error(error: M) -> Self { Self::new(Status::InternalServerError, error) } diff --git a/crates/ezidam/src/routes/oauth/redirect.rs b/crates/ezidam/src/routes/oauth/redirect.rs index 5b82a37..8684e7d 100644 --- a/crates/ezidam/src/routes/oauth/redirect.rs +++ b/crates/ezidam/src/routes/oauth/redirect.rs @@ -1,6 +1,9 @@ use crate::routes::prelude::*; +use apps::App; use authorization_codes::AuthorizationCode; use hash::SecretString; +use jwt::database::Key; +use jwt::{JwtClaims, PrivateKey}; use refresh_tokens::RefreshToken; use rocket::{get, UriDisplayQuery}; use rocket_client_addr::ClientRealAddr; @@ -39,6 +42,11 @@ pub async fn redirect_page( return Err(Error::bad_request("Authorization code has expired")); } + // Get app + let app = App::get_one_from_authorization_code(&mut transaction, redirect_request.code) + .await? + .ok_or_else(|| Error::not_found("Could not find application"))?; + // Get user info let user = User::get_one_from_authorization_code(&mut transaction, redirect_request.code) .await? @@ -73,7 +81,27 @@ pub async fn redirect_page( ) .await?; - // TODO: generate access token + // Get latest key from database + let key = Key::get_most_recent(&mut transaction) + .await? + .ok_or_else(|| Error::internal_server_error("Failed to get key to sign JWT"))?; + + // Make sure key has not been revoked + if key.is_revoked() { + return Err(Error::forbidden("Signing key has been revoked")); + } + + // Import private key + let private_key = + task::spawn_blocking(move || PrivateKey::from_der(key.private_der(), key.key_id())) + .await??; + + // TODO: get user roles + let roles = vec![]; + + // Create jwt, sign and serialize + let jwt = JwtClaims::new(home_page.clone(), app.id().as_ref(), &user, roles) + .sign_serialize(&private_key)?; // TODO: store tokens in secure, http only cookies diff --git a/crates/jwt/Cargo.toml b/crates/jwt/Cargo.toml index 3dd7357..379d25f 100644 --- a/crates/jwt/Cargo.toml +++ b/crates/jwt/Cargo.toml @@ -15,3 +15,4 @@ chrono = { workspace = true } # local crates id = { path = "../id" } database = { path = "../database" } +users = { path = "../users" } \ No newline at end of file diff --git a/crates/jwt/src/claims.rs b/crates/jwt/src/claims.rs new file mode 100644 index 0000000..0da7ff5 --- /dev/null +++ b/crates/jwt/src/claims.rs @@ -0,0 +1,58 @@ +use crate::{Error, PrivateKey}; +use chrono::Duration; +use jwt_compact::{Claims, Header, TimeOptions}; +use serde::{Deserialize, Serialize}; +use users::User; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JwtClaims { + // Standard JWT claims + #[serde(rename = "iss")] + pub issuer: String, + #[serde(rename = "sub")] + pub subject: String, + #[serde(rename = "aud")] + pub audience: String, + + // Custom claims + pub username: String, + pub email: Option, + pub is_admin: bool, + pub roles: Vec, +} + +impl JwtClaims { + pub fn new( + issuer: impl Into, + audience: impl Into, + user: &User, + roles: Vec, + ) -> Self { + Self { + // Standard JWT claims + issuer: issuer.into(), + subject: user.id().to_string(), + audience: audience.into(), + + // Custom claims + username: user.username().to_string(), + email: user.email().map(String::from), + is_admin: user.is_admin(), + roles, + } + } + + pub fn sign_serialize(self, key: &PrivateKey) -> Result { + let header = Header::default().with_key_id(key.id()); + + let claims = Claims::::new(self); + + // Set duration + let duration = Duration::minutes(15); + let time_options = TimeOptions::default(); + let claims = claims.set_duration_and_issuance(&time_options, duration); + + key.sign_serialize_jwt(header, claims) + } +} diff --git a/crates/jwt/src/database.rs b/crates/jwt/src/database.rs index c678964..9f62583 100644 --- a/crates/jwt/src/database.rs +++ b/crates/jwt/src/database.rs @@ -44,6 +44,10 @@ impl Key { pub fn public_der(&self) -> &[u8] { &self.public_der } + + pub fn is_revoked(&self) -> bool { + self.revoked_at.is_some() + } } impl From for Key { diff --git a/crates/jwt/src/error.rs b/crates/jwt/src/error.rs index 92b79cb..54a9746 100644 --- a/crates/jwt/src/error.rs +++ b/crates/jwt/src/error.rs @@ -20,4 +20,7 @@ pub enum Error { #[error("Failed to serialize JWK: `{0}`")] JwkSerialization(#[from] serde_json::Error), + + #[error("Failed to create JWT: `{0}`")] + JwtCreation(#[from] jwt_compact::CreationError), } diff --git a/crates/jwt/src/key/private.rs b/crates/jwt/src/key/private.rs index c85bb62..b5bcf86 100644 --- a/crates/jwt/src/key/private.rs +++ b/crates/jwt/src/key/private.rs @@ -1,6 +1,7 @@ -use crate::Error; +use crate::{Error, JwtClaims}; use id::KeyID; -use jwt_compact::alg::{RsaPrivateKey, StrongKey}; +use jwt_compact::alg::{Rsa, RsaPrivateKey, StrongKey}; +use jwt_compact::{AlgorithmExt, Claims, Header}; use rsa::pkcs8::der::zeroize::Zeroizing; use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey}; @@ -27,6 +28,18 @@ impl PrivateKey { key: RsaPrivateKey::from_pkcs8_der(der)?, }) } + + pub fn id(&self) -> &str { + self.id.as_ref() + } + + pub fn sign_serialize_jwt( + &self, + header: Header, + claims: Claims, + ) -> Result { + Ok(Rsa::ps256().token(header, &claims, &self.key)?) + } } #[cfg(test)] diff --git a/crates/jwt/src/lib.rs b/crates/jwt/src/lib.rs index 6d9dd9c..4949aab 100644 --- a/crates/jwt/src/lib.rs +++ b/crates/jwt/src/lib.rs @@ -1,12 +1,14 @@ extern crate core; +mod claims; pub mod database; mod error; mod jwk; mod key; mod token; -/// Exports +/// Export +pub use claims::JwtClaims; pub use error::Error; pub use key::generate; pub use key::{PrivateKey, PublicKey}; diff --git a/crates/users/src/lib.rs b/crates/users/src/lib.rs index 1ad11ec..da0499b 100644 --- a/crates/users/src/lib.rs +++ b/crates/users/src/lib.rs @@ -37,4 +37,10 @@ impl User { pub fn name(&self) -> Option<&str> { self.name.as_deref() } + pub fn email(&self) -> Option<&str> { + self.email.as_deref() + } + pub fn is_admin(&self) -> bool { + self.is_admin + } }