ezidam + jwt: get key, import private key, create jwt claims and sign them
This commit is contained in:
parent
ef8d75ecee
commit
e99115e174
14 changed files with 217 additions and 5 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1535,6 +1535,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"users",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -58,4 +58,13 @@ impl App {
|
||||||
) -> Result<Option<Self>, Error> {
|
) -> Result<Option<Self>, Error> {
|
||||||
Ok(DatabaseApps::get_one_by_id(conn, id).await?.map(Self::from))
|
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<Option<Self>, Error> {
|
||||||
|
Ok(DatabaseApps::get_one_from_authorization_code(conn, code)
|
||||||
|
.await?
|
||||||
|
.map(Self::from))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
select a.id,
|
||||||
|
a.created_at as "created_at: DateTime<Utc>",
|
||||||
|
a.updated_at as "updated_at: DateTime<Utc>",
|
||||||
|
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 ?
|
||||||
|
|
@ -648,6 +648,66 @@
|
||||||
},
|
},
|
||||||
"query": "select id,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\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"
|
"query": "select id,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\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<Utc>",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at: DateTime<Utc>",
|
||||||
|
"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<Utc>\",\n a.updated_at as \"updated_at: DateTime<Utc>\",\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": {
|
"eb1a0153c88b0b2744ed1b71df04a91a129a0173fbbc3e2536f52d41e8dc99c4": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|
|
||||||
|
|
@ -59,4 +59,18 @@ impl Apps {
|
||||||
.await
|
.await
|
||||||
.map_err(handle_error)
|
.map_err(handle_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_one_from_authorization_code(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
code: &str,
|
||||||
|
) -> Result<Option<Self>, Error> {
|
||||||
|
sqlx::query_file_as!(
|
||||||
|
Self,
|
||||||
|
"queries/apps/get_one_from_authorization_code.sql",
|
||||||
|
code
|
||||||
|
)
|
||||||
|
.fetch_optional(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ impl Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn internal_server_error<E: std::error::Error>(error: E) -> Self {
|
pub fn internal_server_error<M: std::fmt::Display>(error: M) -> Self {
|
||||||
Self::new(Status::InternalServerError, error)
|
Self::new(Status::InternalServerError, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
use crate::routes::prelude::*;
|
use crate::routes::prelude::*;
|
||||||
|
use apps::App;
|
||||||
use authorization_codes::AuthorizationCode;
|
use authorization_codes::AuthorizationCode;
|
||||||
use hash::SecretString;
|
use hash::SecretString;
|
||||||
|
use jwt::database::Key;
|
||||||
|
use jwt::{JwtClaims, PrivateKey};
|
||||||
use refresh_tokens::RefreshToken;
|
use refresh_tokens::RefreshToken;
|
||||||
use rocket::{get, UriDisplayQuery};
|
use rocket::{get, UriDisplayQuery};
|
||||||
use rocket_client_addr::ClientRealAddr;
|
use rocket_client_addr::ClientRealAddr;
|
||||||
|
|
@ -39,6 +42,11 @@ pub async fn redirect_page(
|
||||||
return Err(Error::bad_request("Authorization code has expired"));
|
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
|
// Get user info
|
||||||
let user = User::get_one_from_authorization_code(&mut transaction, redirect_request.code)
|
let user = User::get_one_from_authorization_code(&mut transaction, redirect_request.code)
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -73,7 +81,27 @@ pub async fn redirect_page(
|
||||||
)
|
)
|
||||||
.await?;
|
.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
|
// TODO: store tokens in secure, http only cookies
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,4 @@ chrono = { workspace = true }
|
||||||
# local crates
|
# local crates
|
||||||
id = { path = "../id" }
|
id = { path = "../id" }
|
||||||
database = { path = "../database" }
|
database = { path = "../database" }
|
||||||
|
users = { path = "../users" }
|
||||||
58
crates/jwt/src/claims.rs
Normal file
58
crates/jwt/src/claims.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
pub is_admin: bool,
|
||||||
|
pub roles: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JwtClaims {
|
||||||
|
pub fn new(
|
||||||
|
issuer: impl Into<String>,
|
||||||
|
audience: impl Into<String>,
|
||||||
|
user: &User,
|
||||||
|
roles: Vec<String>,
|
||||||
|
) -> 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<String, Error> {
|
||||||
|
let header = Header::default().with_key_id(key.id());
|
||||||
|
|
||||||
|
let claims = Claims::<Self>::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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,10 @@ impl Key {
|
||||||
pub fn public_der(&self) -> &[u8] {
|
pub fn public_der(&self) -> &[u8] {
|
||||||
&self.public_der
|
&self.public_der
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_revoked(&self) -> bool {
|
||||||
|
self.revoked_at.is_some()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DatabaseKeys> for Key {
|
impl From<DatabaseKeys> for Key {
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,7 @@ pub enum Error {
|
||||||
|
|
||||||
#[error("Failed to serialize JWK: `{0}`")]
|
#[error("Failed to serialize JWK: `{0}`")]
|
||||||
JwkSerialization(#[from] serde_json::Error),
|
JwkSerialization(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("Failed to create JWT: `{0}`")]
|
||||||
|
JwtCreation(#[from] jwt_compact::CreationError),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::Error;
|
use crate::{Error, JwtClaims};
|
||||||
use id::KeyID;
|
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::der::zeroize::Zeroizing;
|
||||||
use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey};
|
use rsa::pkcs8::{DecodePrivateKey, EncodePrivateKey};
|
||||||
|
|
||||||
|
|
@ -27,6 +28,18 @@ impl PrivateKey {
|
||||||
key: RsaPrivateKey::from_pkcs8_der(der)?,
|
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<JwtClaims>,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
Ok(Rsa::ps256().token(header, &claims, &self.key)?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
extern crate core;
|
extern crate core;
|
||||||
|
|
||||||
|
mod claims;
|
||||||
pub mod database;
|
pub mod database;
|
||||||
mod error;
|
mod error;
|
||||||
mod jwk;
|
mod jwk;
|
||||||
mod key;
|
mod key;
|
||||||
mod token;
|
mod token;
|
||||||
|
|
||||||
/// Exports
|
/// Export
|
||||||
|
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};
|
||||||
|
|
|
||||||
|
|
@ -37,4 +37,10 @@ impl User {
|
||||||
pub fn name(&self) -> Option<&str> {
|
pub fn name(&self) -> Option<&str> {
|
||||||
self.name.as_deref()
|
self.name.as_deref()
|
||||||
}
|
}
|
||||||
|
pub fn email(&self) -> Option<&str> {
|
||||||
|
self.email.as_deref()
|
||||||
|
}
|
||||||
|
pub fn is_admin(&self) -> bool {
|
||||||
|
self.is_admin
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue