From 66494e964278fb027598981c4d26d39693f6b11e Mon Sep 17 00:00:00 2001 From: augustuswm Date: Wed, 15 Apr 2026 16:46:03 -0500 Subject: [PATCH 1/2] Generalize jwt impl and provide simulated extractor --- v-api/src/authn/jwt.rs | 41 +++++++++++++++++++++++++++++++++++------ v-api/src/authn/mod.rs | 6 ++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/v-api/src/authn/jwt.rs b/v-api/src/authn/jwt.rs index 8733f009..4ec68484 100644 --- a/v-api/src/authn/jwt.rs +++ b/v-api/src/authn/jwt.rs @@ -4,19 +4,21 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use chrono::{DateTime, Utc}; +use dropshot::{RequestContext, SharedExtractor}; +use dropshot_authorization_header::bearer::BearerAuth; use jsonwebtoken::{ decode, decode_header, jwk::{AlgorithmParameters, Jwk}, Algorithm, DecodingKey, Header, Validation, }; use newtype_uuid::TypedUuid; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{fmt::Debug, sync::Arc}; use thiserror::Error; use tracing::instrument; use v_model::{AccessTokenId, UserId, UserProviderId}; -use crate::{authn::Signer, context::VContext, permissions::VAppPermission}; +use crate::{authn::Signer, context::VContext, permissions::VAppPermission, ApiContext}; use super::SigningKeyError; @@ -34,13 +36,15 @@ pub enum JwtError { MissingKid, #[error("Failed to find a matching key as requested by token")] NoMatchingKey, + #[error("No token found")] + NoToken, #[error("Unsupported algorithm")] UnsupportedAlgorithm, } #[derive(Debug, Deserialize, Serialize)] -pub struct Jwt { - pub claims: Claims, +pub struct Jwt { + pub claims: T, } #[derive(Debug, Deserialize, Serialize)] @@ -80,7 +84,10 @@ impl Claims { } } -impl Jwt { +impl Jwt +where + C: Debug + DeserializeOwned + Serialize, +{ pub async fn new(ctx: &VContext, token: &str) -> Result where T: VAppPermission, @@ -103,7 +110,7 @@ impl Jwt { // The only JWKs supported are those that are available in the server context let jwk = ctx.jwks().await.find(&kid).ok_or(JwtError::NoMatchingKey)?; let (key, algorithm) = DecodingKey::from_jwk(jwk) - .map(|key| (key, Jwt::algo(jwk))) + .map(|key| (key, Jwt::::algo(jwk))) .map_err(JwtError::InvalidJwk)?; tracing::trace!(?jwk, ?algorithm, "Kid matched known decoding key"); @@ -131,6 +138,28 @@ impl Jwt { } } } + + // Extract an JWT from a Dropshot request + pub async fn extract( + rqctx: &RequestContext>, + ) -> Result + where + T: VAppPermission, + { + // Ensure there is a bearer, without it there is nothing else to do + let bearer = BearerAuth::from_request(rqctx).await.map_err(|err| { + tracing::info!(?err, "Failed to extract bearer auth"); + JwtError::NoToken + })?; + + // Check that the extracted token actually contains a value + let token = bearer.consume().ok_or_else(|| { + tracing::debug!("Bearer auth is empty"); + JwtError::NoToken + })?; + + Self::new(&rqctx.v_ctx(), &token).await + } } #[derive(Debug, Error)] diff --git a/v-api/src/authn/mod.rs b/v-api/src/authn/mod.rs index 9820a857..ee4b6ac2 100644 --- a/v-api/src/authn/mod.rs +++ b/v-api/src/authn/mod.rs @@ -24,7 +24,9 @@ use v_api_param::ParamResolutionError; use v_model::permissions::PermissionStorage; use crate::{ - authn::key::RawKey, context::ApiContext, permissions::VAppPermission, + authn::{jwt::Claims, key::RawKey}, + context::ApiContext, + permissions::VAppPermission, util::response::unauthorized, }; @@ -44,7 +46,7 @@ pub enum AuthError { // A token that provides authentication and optionally (JWT) authorization claims pub enum AuthToken { ApiKey(RawKey), - Jwt(Jwt), + Jwt(Jwt), } impl Debug for AuthToken { From 0e3a99093a0ed5e2a83a926b953ae5482cffd8d6 Mon Sep 17 00:00:00 2001 From: augustuswm Date: Wed, 15 Apr 2026 16:49:25 -0500 Subject: [PATCH 2/2] Fix unnecessary ref --- v-api/src/authn/jwt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v-api/src/authn/jwt.rs b/v-api/src/authn/jwt.rs index 4ec68484..cc57bda2 100644 --- a/v-api/src/authn/jwt.rs +++ b/v-api/src/authn/jwt.rs @@ -158,7 +158,7 @@ where JwtError::NoToken })?; - Self::new(&rqctx.v_ctx(), &token).await + Self::new(rqctx.v_ctx(), &token).await } }