cleanup: strip comments, extract constants, DRY shared helpers across adapters + infra-wiring
- strip all comments except WHY workaround notes (3 remain) - remove all #[allow(dead_code)]; fix via _prefix rename - extract named constants: JWT time units, token types, default config values, jellyfin fallback bitrate - DRY: move serialize_enum_as_string, content_type_str, parse_content_type, parse_genres_blob to adapter-common - sqlite+postgres library.rs use shared helpers instead of local copies - sqlite+postgres channel.rs use shared serialize_enum_as_string - remove dead `let _ = ext` in scanner.rs
This commit is contained in:
@@ -1,40 +1,24 @@
|
||||
//! JWT token generation and validation (HS256).
|
||||
//!
|
||||
//! This does NOT implement a domain port — it is used directly by the
|
||||
//! presentation layer's auth extractors.
|
||||
|
||||
use domain::User;
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Minimum secret length for production (256 bits = 32 bytes).
|
||||
const MIN_SECRET_LENGTH: usize = 32;
|
||||
const SECS_PER_HOUR: usize = 3600;
|
||||
const SECS_PER_DAY: usize = 86400;
|
||||
const TOKEN_TYPE_ACCESS: &str = "access";
|
||||
const TOKEN_TYPE_REFRESH: &str = "refresh";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// JWT configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JwtConfig {
|
||||
/// Secret key for HS256 signing/verification.
|
||||
pub secret: String,
|
||||
/// Expected issuer (for validation).
|
||||
pub issuer: Option<String>,
|
||||
/// Expected audience (for validation).
|
||||
pub audience: Option<String>,
|
||||
/// Access token expiry in hours (default: 24).
|
||||
pub expiry_hours: u64,
|
||||
/// Refresh token expiry in days (default: 30).
|
||||
pub refresh_expiry_days: u64,
|
||||
}
|
||||
|
||||
impl JwtConfig {
|
||||
/// Create a new JWT config with validation.
|
||||
///
|
||||
/// In production mode, this rejects secrets shorter than
|
||||
/// [`MIN_SECRET_LENGTH`] bytes.
|
||||
pub fn new(
|
||||
secret: String,
|
||||
issuer: Option<String>,
|
||||
@@ -59,7 +43,6 @@ impl JwtConfig {
|
||||
})
|
||||
}
|
||||
|
||||
/// Create config without validation (for testing).
|
||||
pub fn new_unchecked(secret: String) -> Self {
|
||||
Self {
|
||||
secret,
|
||||
@@ -71,42 +54,24 @@ impl JwtConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Claims
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn default_token_type() -> String {
|
||||
"access".to_string()
|
||||
TOKEN_TYPE_ACCESS.to_string()
|
||||
}
|
||||
|
||||
/// JWT claims structure.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct JwtClaims {
|
||||
/// Subject — the user's unique identifier (user ID as string).
|
||||
pub sub: String,
|
||||
/// User's email address.
|
||||
pub email: String,
|
||||
/// Expiry timestamp (seconds since UNIX epoch).
|
||||
pub exp: usize,
|
||||
/// Issued-at timestamp (seconds since UNIX epoch).
|
||||
pub iat: usize,
|
||||
/// Issuer.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iss: Option<String>,
|
||||
/// Audience.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aud: Option<String>,
|
||||
/// Token type: `"access"` or `"refresh"`. Defaults to `"access"` for
|
||||
/// backward compatibility.
|
||||
#[serde(default = "default_token_type")]
|
||||
pub token_type: String,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// JWT-related errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum JwtError {
|
||||
#[error("JWT secret too weak: minimum {min_length} bytes, got {actual_length}")]
|
||||
@@ -131,11 +96,6 @@ pub enum JwtError {
|
||||
MissingConfig,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Validator / generator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// JWT token validator and generator.
|
||||
#[derive(Clone)]
|
||||
pub struct JwtValidator {
|
||||
config: JwtConfig,
|
||||
@@ -145,7 +105,6 @@ pub struct JwtValidator {
|
||||
}
|
||||
|
||||
impl JwtValidator {
|
||||
/// Create a new JWT validator with the given configuration.
|
||||
pub fn new(config: JwtConfig) -> Self {
|
||||
let encoding_key = EncodingKey::from_secret(config.secret.as_bytes());
|
||||
let decoding_key = DecodingKey::from_secret(config.secret.as_bytes());
|
||||
@@ -167,10 +126,9 @@ impl JwtValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an access JWT token for the given user.
|
||||
pub fn create_token(&self, user: &User) -> Result<String, JwtError> {
|
||||
let now = now_secs();
|
||||
let expiry = now + (self.config.expiry_hours as usize * 3600);
|
||||
let expiry = now + (self.config.expiry_hours as usize * SECS_PER_HOUR);
|
||||
|
||||
let claims = JwtClaims {
|
||||
sub: user.id().to_string(),
|
||||
@@ -179,17 +137,16 @@ impl JwtValidator {
|
||||
iat: now,
|
||||
iss: self.config.issuer.clone(),
|
||||
aud: self.config.audience.clone(),
|
||||
token_type: "access".to_string(),
|
||||
token_type: TOKEN_TYPE_ACCESS.to_string(),
|
||||
};
|
||||
|
||||
encode(&Header::new(Algorithm::HS256), &claims, &self.encoding_key)
|
||||
.map_err(JwtError::CreationFailed)
|
||||
}
|
||||
|
||||
/// Create a refresh JWT token for the given user (longer-lived).
|
||||
pub fn create_refresh_token(&self, user: &User) -> Result<String, JwtError> {
|
||||
let now = now_secs();
|
||||
let expiry = now + (self.config.refresh_expiry_days as usize * 86400);
|
||||
let expiry = now + (self.config.refresh_expiry_days as usize * SECS_PER_DAY);
|
||||
|
||||
let claims = JwtClaims {
|
||||
sub: user.id().to_string(),
|
||||
@@ -198,14 +155,13 @@ impl JwtValidator {
|
||||
iat: now,
|
||||
iss: self.config.issuer.clone(),
|
||||
aud: self.config.audience.clone(),
|
||||
token_type: "refresh".to_string(),
|
||||
token_type: TOKEN_TYPE_REFRESH.to_string(),
|
||||
};
|
||||
|
||||
encode(&Header::new(Algorithm::HS256), &claims, &self.encoding_key)
|
||||
.map_err(JwtError::CreationFailed)
|
||||
}
|
||||
|
||||
/// Validate a JWT token and return the claims.
|
||||
pub fn validate_token(&self, token: &str) -> Result<JwtClaims, JwtError> {
|
||||
let token_data =
|
||||
decode::<JwtClaims>(token, &self.decoding_key, &self.validation).map_err(|e| {
|
||||
@@ -219,10 +175,9 @@ impl JwtValidator {
|
||||
Ok(token_data.claims)
|
||||
}
|
||||
|
||||
/// Validate an access token — rejects refresh tokens.
|
||||
pub fn validate_access_token(&self, token: &str) -> Result<JwtClaims, JwtError> {
|
||||
let claims = self.validate_token(token)?;
|
||||
if claims.token_type != "access" {
|
||||
if claims.token_type != TOKEN_TYPE_ACCESS {
|
||||
return Err(JwtError::ValidationFailed(
|
||||
"Not an access token".to_string(),
|
||||
));
|
||||
@@ -230,10 +185,9 @@ impl JwtValidator {
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
/// Validate a refresh token — rejects access tokens.
|
||||
pub fn validate_refresh_token(&self, token: &str) -> Result<JwtClaims, JwtError> {
|
||||
let claims = self.validate_token(token)?;
|
||||
if claims.token_type != "refresh" {
|
||||
if claims.token_type != TOKEN_TYPE_REFRESH {
|
||||
return Err(JwtError::ValidationFailed(
|
||||
"Not a refresh token".to_string(),
|
||||
));
|
||||
@@ -241,9 +195,6 @@ impl JwtValidator {
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
/// Get the user ID (subject) from a token without full validation.
|
||||
///
|
||||
/// Useful for logging/debugging — should not be trusted for auth decisions.
|
||||
pub fn decode_unverified(&self, token: &str) -> Result<JwtClaims, JwtError> {
|
||||
let mut insecure = Validation::new(Algorithm::HS256);
|
||||
insecure.insecure_disable_signature_validation();
|
||||
@@ -273,10 +224,6 @@ fn now_secs() -> usize {
|
||||
.as_secs() as usize
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -311,7 +258,6 @@ mod tests {
|
||||
let claims = validator.validate_refresh_token(&token).unwrap();
|
||||
assert_eq!(claims.token_type, "refresh");
|
||||
|
||||
// Access-only validation rejects it
|
||||
assert!(validator.validate_access_token(&token).is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//! Auth adapter crate — JWT, OIDC, and password hashing.
|
||||
|
||||
pub mod password;
|
||||
|
||||
#[cfg(feature = "jwt")]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//! OIDC (OpenID Connect) authorization flow adapter.
|
||||
|
||||
use domain::{
|
||||
AuthorizationCode, AuthorizationUrlData, ClientId, ClientSecret, CsrfToken, IssuerUrl,
|
||||
OidcNonce, PkceVerifier, RedirectUrl, ResourceId,
|
||||
@@ -18,10 +16,6 @@ use openidconnect::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type aliases
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub type OidcClient = Client<
|
||||
EmptyAdditionalClaims,
|
||||
CoreAuthDisplay,
|
||||
@@ -34,19 +28,14 @@ pub type OidcClient = Client<
|
||||
CoreTokenIntrospectionResponse,
|
||||
CoreRevocableToken,
|
||||
CoreRevocationErrorResponse,
|
||||
EndpointSet, // HasAuthUrl
|
||||
EndpointNotSet, // HasDeviceAuthUrl
|
||||
EndpointNotSet, // HasIntrospectionUrl
|
||||
EndpointNotSet, // HasRevocationUrl
|
||||
EndpointMaybeSet, // HasTokenUrl
|
||||
EndpointMaybeSet, // HasUserInfoUrl
|
||||
EndpointSet,
|
||||
EndpointNotSet,
|
||||
EndpointNotSet,
|
||||
EndpointNotSet,
|
||||
EndpointMaybeSet,
|
||||
EndpointMaybeSet,
|
||||
>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// OIDC-specific errors.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum OidcError {
|
||||
#[error("OIDC discovery failed: {0}")]
|
||||
@@ -71,11 +60,6 @@ pub enum OidcError {
|
||||
Http(String),
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State / types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Serializable OIDC state stored in an encrypted cookie during the auth flow.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct OidcState {
|
||||
pub csrf_token: CsrfToken,
|
||||
@@ -83,18 +67,12 @@ pub struct OidcState {
|
||||
pub pkce_verifier: PkceVerifier,
|
||||
}
|
||||
|
||||
/// Resolved OIDC user info.
|
||||
#[derive(Debug)]
|
||||
pub struct OidcUser {
|
||||
pub subject: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// OIDC authorization flow service.
|
||||
#[derive(Clone)]
|
||||
pub struct OidcService {
|
||||
client: OidcClient,
|
||||
@@ -103,7 +81,6 @@ pub struct OidcService {
|
||||
}
|
||||
|
||||
impl OidcService {
|
||||
/// Create a new OIDC service — performs provider discovery.
|
||||
pub async fn new(
|
||||
issuer: IssuerUrl,
|
||||
client_id: ClientId,
|
||||
@@ -157,11 +134,6 @@ impl OidcService {
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the authorization URL and associated state for OIDC login.
|
||||
///
|
||||
/// Returns `(AuthorizationUrlData, OidcState)` — the state should be
|
||||
/// serialized and stored in an encrypted cookie for the duration of the
|
||||
/// flow.
|
||||
pub fn get_authorization_url(&self) -> (AuthorizationUrlData, OidcState) {
|
||||
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
@@ -193,8 +165,6 @@ impl OidcService {
|
||||
(auth_data, oidc_state)
|
||||
}
|
||||
|
||||
/// Resolve the OIDC callback — exchange code for tokens, verify ID token,
|
||||
/// and return the authenticated user.
|
||||
pub async fn resolve_callback(
|
||||
&self,
|
||||
code: AuthorizationCode,
|
||||
@@ -232,7 +202,6 @@ impl OidcService {
|
||||
.claims(&id_token_verifier, &oidc_nonce)
|
||||
.map_err(|e| OidcError::IdTokenVerification(e.to_string()))?;
|
||||
|
||||
// Verify access token hash if present
|
||||
if let Some(expected_hash) = claims.access_token_hash() {
|
||||
let actual_hash = AccessTokenHash::from_token(
|
||||
token_response.access_token(),
|
||||
@@ -250,7 +219,6 @@ impl OidcService {
|
||||
}
|
||||
}
|
||||
|
||||
// Get email from ID token or fall back to UserInfo endpoint
|
||||
let email = if let Some(email) = claims.email() {
|
||||
Some(email.as_str().to_string())
|
||||
} else {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
//! Password hashing adapter using the `password-auth` crate.
|
||||
|
||||
use domain::errors::DomainResult;
|
||||
use domain::ports::AuthService;
|
||||
|
||||
/// Concrete `AuthService` implementation backed by `password-auth`
|
||||
/// (Argon2id by default).
|
||||
pub struct PasswordAuthService;
|
||||
|
||||
impl AuthService for PasswordAuthService {
|
||||
|
||||
Reference in New Issue
Block a user