server: Move auth logic to host module
In preparation for adding a separate authorization strategy for client requests, I have moved the implementation of the authorization strategy for host requests in to the `server::host` module.master
parent
818cfc94c2
commit
839d756a28
19
src/auth.rs
19
src/auth.rs
|
@ -19,7 +19,7 @@ use uuid::Uuid;
|
|||
|
||||
/// JWT Token Claims
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub struct HostClaims {
|
||||
/// Token subject (machine hostname)
|
||||
pub sub: String,
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ pub fn get_token_subject(token: &str) -> Result<String> {
|
|||
v.insecure_disable_signature_validation();
|
||||
v.set_required_spec_claims(&["sub"]);
|
||||
let k = DecodingKey::from_secret(b"");
|
||||
let data: TokenData<Claims> = decode(token, &k, &v)?;
|
||||
let data: TokenData<HostClaims> = decode(token, &k, &v)?;
|
||||
Ok(data.claims.sub)
|
||||
}
|
||||
|
||||
|
@ -46,12 +46,12 @@ pub fn get_token_subject(token: &str) -> Result<String> {
|
|||
/// `service` argument, and is within its validity period (not before/expires).
|
||||
/// The token must be signed with HMAC-SHA256 using the host's machine ID as
|
||||
/// the secret key.
|
||||
pub fn validate_token(
|
||||
pub fn validate_host_token(
|
||||
token: &str,
|
||||
hostname: &str,
|
||||
machine_id: &Uuid,
|
||||
service: &str,
|
||||
) -> Result<Claims> {
|
||||
) -> Result<HostClaims> {
|
||||
let mut v = Validation::new(Algorithm::HS256);
|
||||
v.validate_nbf = true;
|
||||
v.set_issuer(&[hostname]);
|
||||
|
@ -66,7 +66,7 @@ pub fn validate_token(
|
|||
OsRng.fill_bytes(&mut secret);
|
||||
}
|
||||
let k = DecodingKey::from_secret(&secret);
|
||||
let data: TokenData<Claims> = decode(token, &k, &v)?;
|
||||
let data: TokenData<HostClaims> = decode(token, &k, &v)?;
|
||||
Ok(data.claims)
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,12 @@ pub(crate) mod test {
|
|||
|
||||
let machine_id = uuid!("9afd42e5-4ac3-4530-90c4-191869063ef9");
|
||||
let token = make_token(hostname, machine_id);
|
||||
validate_token(&token, hostname, &machine_id, "sshca.example.org")
|
||||
.unwrap();
|
||||
validate_host_token(
|
||||
&token,
|
||||
hostname,
|
||||
&machine_id,
|
||||
"sshca.example.org",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use axum::async_trait;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::headers::authorization::Bearer;
|
||||
use axum::headers::{Authorization, Host};
|
||||
use axum::http::request::Parts;
|
||||
use axum::extract::multipart::{Multipart, MultipartError};
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::{RequestPartsExt, TypedHeader};
|
||||
use serde::Serialize;
|
||||
use ssh_key::Algorithm;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::Claims;
|
||||
use crate::auth::{self, HostClaims};
|
||||
use crate::ca;
|
||||
use crate::machine_id;
|
||||
use super::{AuthError, Context};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SignKeyResponse {
|
||||
|
@ -80,6 +90,71 @@ impl IntoResponse for SignKeyError {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FromRequestParts<Arc<Context>> for HostClaims {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
ctx: &super::State,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let TypedHeader(Authorization(bearer)) = parts
|
||||
.extract::<TypedHeader<Authorization<Bearer>>>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
debug!("Failed to extract token from HTTP request: {}", e);
|
||||
AuthError
|
||||
})?;
|
||||
let host = parts.extract::<TypedHeader<Host>>().await.map_or_else(
|
||||
|_| "localhost".to_owned(),
|
||||
|v| v.hostname().to_owned(),
|
||||
);
|
||||
|
||||
let hostname =
|
||||
auth::get_token_subject(bearer.token()).map_err(|e| {
|
||||
debug!("Could not get token subject: {}", e);
|
||||
AuthError
|
||||
})?;
|
||||
let machine_id =
|
||||
get_machine_id(&hostname, ctx).await.ok_or_else(|| {
|
||||
debug!("No machine ID found for host {}", hostname);
|
||||
AuthError
|
||||
})?;
|
||||
let claims = auth::validate_host_token(
|
||||
bearer.token(),
|
||||
&hostname,
|
||||
&machine_id,
|
||||
&host,
|
||||
)
|
||||
.map_err(|e| {
|
||||
debug!("Invalid auth token: {}", e);
|
||||
AuthError
|
||||
})?;
|
||||
debug!("Successfully authenticated request from host {}", hostname);
|
||||
Ok(claims)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_machine_id(hostname: &str, ctx: &super::State) -> Option<Uuid> {
|
||||
let cache = ctx.cache.read().await;
|
||||
if let Some((ts, m)) = cache.get(hostname) {
|
||||
if ts.elapsed() < Duration::from_secs(60) {
|
||||
debug!("Found cached machine ID for {}", hostname);
|
||||
return Some(*m);
|
||||
} else {
|
||||
debug!("Cached machine ID for {} has expired", hostname);
|
||||
}
|
||||
}
|
||||
drop(cache);
|
||||
let machine_id =
|
||||
machine_id::get_machine_id(hostname, ctx.config.clone()).await?;
|
||||
let mut cache = ctx.cache.write().await;
|
||||
debug!("Caching machine ID for {}", hostname);
|
||||
cache.insert(hostname.into(), (Instant::now(), machine_id));
|
||||
Some(machine_id)
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
struct SignKeyRequest {
|
||||
hostname: String,
|
||||
|
@ -87,7 +162,7 @@ struct SignKeyRequest {
|
|||
}
|
||||
|
||||
pub(super) async fn sign_host_cert(
|
||||
claims: Claims,
|
||||
claims: HostClaims,
|
||||
State(ctx): State<super::State>,
|
||||
mut form: Multipart,
|
||||
) -> Result<String, SignKeyError> {
|
||||
|
@ -136,8 +211,7 @@ pub(super) async fn sign_host_cert(
|
|||
pubkey.algorithm().as_str(),
|
||||
hostname
|
||||
);
|
||||
let cert =
|
||||
ca::sign_cert(&hostname, &pubkey, duration, &privkey, &[])?;
|
||||
let cert = ca::sign_cert(&hostname, &pubkey, duration, &privkey, &[])?;
|
||||
info!(
|
||||
"Signed {} key for {}",
|
||||
pubkey.algorithm().as_str(),
|
||||
|
|
|
@ -2,24 +2,16 @@ mod host;
|
|||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Instant;
|
||||
|
||||
use axum::async_trait;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::headers::authorization::Bearer;
|
||||
use axum::headers::{Authorization, Host};
|
||||
use axum::http::request::Parts;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{RequestPartsExt, Router, TypedHeader};
|
||||
use axum::Router;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::debug;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::{self, Claims};
|
||||
use crate::config::Configuration;
|
||||
use crate::machine_id;
|
||||
|
||||
struct Context {
|
||||
config: Arc<Configuration>,
|
||||
|
@ -36,51 +28,6 @@ impl IntoResponse for AuthError {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FromRequestParts<Arc<Context>> for Claims {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
ctx: &State,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let TypedHeader(Authorization(bearer)) = parts
|
||||
.extract::<TypedHeader<Authorization<Bearer>>>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
debug!("Failed to extract token from HTTP request: {}", e);
|
||||
AuthError
|
||||
})?;
|
||||
let host = parts.extract::<TypedHeader<Host>>().await.map_or_else(
|
||||
|_| "localhost".to_owned(),
|
||||
|v| v.hostname().to_owned(),
|
||||
);
|
||||
|
||||
let hostname =
|
||||
auth::get_token_subject(bearer.token()).map_err(|e| {
|
||||
debug!("Could not get token subject: {}", e);
|
||||
AuthError
|
||||
})?;
|
||||
let machine_id =
|
||||
get_machine_id(&hostname, ctx).await.ok_or_else(|| {
|
||||
debug!("No machine ID found for host {}", hostname);
|
||||
AuthError
|
||||
})?;
|
||||
let claims = auth::validate_token(
|
||||
bearer.token(),
|
||||
&hostname,
|
||||
&machine_id,
|
||||
&host,
|
||||
)
|
||||
.map_err(|e| {
|
||||
debug!("Invalid auth token: {}", e);
|
||||
AuthError
|
||||
})?;
|
||||
debug!("Successfully authenticated request from host {}", hostname);
|
||||
Ok(claims)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_app(config: Configuration) -> Router {
|
||||
let ctx = Arc::new(Context {
|
||||
config: config.into(),
|
||||
|
@ -92,25 +39,6 @@ pub fn make_app(config: Configuration) -> Router {
|
|||
.with_state(ctx)
|
||||
}
|
||||
|
||||
async fn get_machine_id(hostname: &str, ctx: &State) -> Option<Uuid> {
|
||||
let cache = ctx.cache.read().await;
|
||||
if let Some((ts, m)) = cache.get(hostname) {
|
||||
if ts.elapsed() < Duration::from_secs(60) {
|
||||
debug!("Found cached machine ID for {}", hostname);
|
||||
return Some(*m);
|
||||
} else {
|
||||
debug!("Cached machine ID for {} has expired", hostname);
|
||||
}
|
||||
}
|
||||
drop(cache);
|
||||
let machine_id =
|
||||
machine_id::get_machine_id(hostname, ctx.config.clone()).await?;
|
||||
let mut cache = ctx.cache.write().await;
|
||||
debug!("Caching machine ID for {}", hostname);
|
||||
cache.insert(hostname.into(), (Instant::now(), machine_id));
|
||||
Some(machine_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use axum::body::Body;
|
||||
|
|
Loading…
Reference in New Issue