blueprint_auth/
auth_token.rs1use axum::http::StatusCode;
2use std::collections::BTreeMap;
3
4use crate::{
5 api_tokens::{ApiToken, ParseApiTokenError},
6 paseto_tokens::{AccessTokenClaims, PasetoError},
7 types::ServiceId,
8};
9
10#[derive(Debug, Clone)]
12pub enum AuthToken {
13 Legacy(ApiToken),
15 ApiKey(String),
17 AccessToken(AccessTokenClaims),
19}
20
21#[derive(Debug, thiserror::Error)]
22pub enum AuthTokenError {
23 #[error("Invalid token format")]
24 InvalidFormat,
25
26 #[error("Legacy token error: {0}")]
27 LegacyToken(#[from] ParseApiTokenError),
28
29 #[error("Paseto token error: {0}")]
30 PasetoToken(#[from] PasetoError),
31
32 #[error("Malformed API key")]
33 MalformedApiKey,
34}
35
36impl AuthToken {
37 pub fn parse(token_str: &str) -> Result<Self, AuthTokenError> {
39 if token_str.starts_with("v4.local.") {
40 return Err(AuthTokenError::InvalidFormat); } else if token_str.contains('|') {
44 let legacy_token = ApiToken::from_str(token_str)?;
46 return Ok(AuthToken::Legacy(legacy_token));
47 } else if token_str.contains('.') {
48 return Ok(AuthToken::ApiKey(token_str.to_string()));
51 }
52
53 Err(AuthTokenError::InvalidFormat)
54 }
55
56 pub fn service_id(&self) -> Option<ServiceId> {
58 match self {
59 AuthToken::Legacy(_) => None, AuthToken::ApiKey(_) => None, AuthToken::AccessToken(claims) => Some(claims.service_id),
62 }
63 }
64
65 pub fn additional_headers(&self) -> BTreeMap<String, String> {
67 match self {
68 AuthToken::AccessToken(claims) => claims.additional_headers.clone(),
69 _ => BTreeMap::new(),
70 }
71 }
72
73 pub fn is_expired(&self) -> bool {
75 match self {
76 AuthToken::AccessToken(claims) => claims.is_expired(),
77 _ => false, }
79 }
80}
81
82#[derive(Debug)]
84pub enum TokenExtractionResult {
85 Legacy(ApiToken),
87 ApiKey(String),
89 ValidatedAccessToken(AccessTokenClaims),
91}
92
93impl<S> axum::extract::FromRequestParts<S> for AuthToken
94where
95 S: Send + Sync,
96{
97 type Rejection = axum::response::Response;
98
99 async fn from_request_parts(
100 parts: &mut axum::http::request::Parts,
101 _state: &S,
102 ) -> Result<Self, Self::Rejection> {
103 use axum::response::IntoResponse;
104
105 let header = match parts.headers.get(crate::types::headers::AUTHORIZATION) {
106 Some(header) => header,
107 None => {
108 return Err(
109 (StatusCode::UNAUTHORIZED, "Missing Authorization header").into_response()
110 );
111 }
112 };
113
114 let header_str = match header.to_str() {
115 Ok(header_str) if header_str.starts_with("Bearer ") => &header_str[7..],
116 Ok(_) => {
117 return Err((
118 StatusCode::BAD_REQUEST,
119 "Invalid Authorization header; expected Bearer token",
120 )
121 .into_response());
122 }
123 Err(_) => {
124 return Err((
125 StatusCode::BAD_REQUEST,
126 "Invalid Authorization header; not valid UTF-8",
127 )
128 .into_response());
129 }
130 };
131
132 match AuthToken::parse(header_str) {
133 Ok(token) => Ok(token),
134 Err(AuthTokenError::InvalidFormat) => {
135 if header_str.starts_with("v4.local.") {
137 Err(
140 (StatusCode::BAD_REQUEST, "Paseto token validation required")
141 .into_response(),
142 )
143 } else {
144 Err((StatusCode::BAD_REQUEST, "Invalid token format").into_response())
145 }
146 }
147 Err(e) => Err((StatusCode::BAD_REQUEST, format!("Invalid token: {e}")).into_response()),
148 }
149 }
150}
151
152#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
154pub struct TokenExchangeRequest {
155 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
157 pub additional_headers: BTreeMap<String, String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub ttl_seconds: Option<u64>,
161}
162
163#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
165pub struct TokenExchangeResponse {
166 pub access_token: String,
168 pub token_type: String,
170 pub expires_at: u64,
172 pub expires_in: u64,
174}
175
176impl TokenExchangeResponse {
177 pub fn new(access_token: String, expires_at: u64) -> Self {
178 let now = std::time::SystemTime::now()
179 .duration_since(std::time::UNIX_EPOCH)
180 .unwrap_or_default()
181 .as_secs();
182
183 Self {
184 access_token,
185 token_type: "Bearer".to_string(),
186 expires_at,
187 expires_in: expires_at.saturating_sub(now),
188 }
189 }
190}