1use std::{sync::Arc, time::Duration};
4
5use axum::{
6 Json,
7 extract::FromRequestParts,
8 http::{StatusCode, header, request::Parts},
9 response::{IntoResponse, Response},
10};
11use pasetors::{
12 Public,
13 claims::{Claims as PasetoClaims, ClaimsValidationRules},
14 keys::{AsymmetricPublicKey, AsymmetricSecretKey},
15 public,
16 token::UntrustedToken,
17 version4::V4,
18};
19
20use crate::{AuthError, DeviceKey, PublicKey, protocol::Claims};
21
22const IMPLICIT_ASSERTION: &[u8] = b"edda/v1/token";
25const CLAIMS_KEY: &str = "edda";
26
27pub struct TokenSigner {
29 secret: AsymmetricSecretKey<V4>,
30 public: PublicKey,
31}
32
33impl TokenSigner {
34 pub fn new(key: &DeviceKey) -> Self {
36 Self {
37 secret: AsymmetricSecretKey::from(&key.keypair_bytes()).expect("64-byte keypair"),
38 public: key.public(),
39 }
40 }
41
42 pub fn public_key(&self) -> PublicKey {
44 self.public
45 }
46
47 pub fn issue(&self, claims: &Claims) -> Result<String, AuthError> {
49 let lifetime = (claims.expires_at - chrono::Utc::now())
50 .to_std()
51 .unwrap_or(Duration::ZERO);
52 let encoding = |e: pasetors::errors::Error| AuthError::Encoding(e.to_string());
53 let mut paseto = PasetoClaims::new_expires_in(&lifetime).map_err(encoding)?;
54 let ours = serde_json::to_value(claims).map_err(|e| AuthError::Encoding(e.to_string()))?;
55 paseto.add_additional(CLAIMS_KEY, ours).map_err(encoding)?;
56 public::sign(&self.secret, &paseto, None, Some(IMPLICIT_ASSERTION)).map_err(encoding)
57 }
58}
59
60pub struct TokenVerifier {
62 public: AsymmetricPublicKey<V4>,
63}
64
65impl TokenVerifier {
66 pub fn new(key: PublicKey) -> Self {
68 Self {
69 public: AsymmetricPublicKey::from(key.as_bytes()).expect("32-byte key"),
70 }
71 }
72
73 pub fn verify(&self, token: &str) -> Result<Claims, AuthError> {
75 let untrusted =
76 UntrustedToken::<Public, V4>::try_from(token).map_err(|_| AuthError::InvalidToken)?;
77 let trusted = public::verify(
79 &self.public,
80 &untrusted,
81 &ClaimsValidationRules::new(),
82 None,
83 Some(IMPLICIT_ASSERTION),
84 )
85 .map_err(|_| AuthError::InvalidToken)?;
86 let ours = trusted
87 .payload_claims()
88 .and_then(|c| c.get_claim(CLAIMS_KEY))
89 .ok_or(AuthError::InvalidToken)?;
90 serde_json::from_value(ours.clone()).map_err(|_| AuthError::InvalidToken)
91 }
92}
93
94pub struct Authenticated(pub Claims);
98
99impl<S: Send + Sync> FromRequestParts<S> for Authenticated {
100 type Rejection = Refused;
101
102 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
103 let verifier = parts
104 .extensions
105 .get::<Arc<TokenVerifier>>()
106 .expect("TokenVerifier extension is installed");
107 let token = parts
108 .headers
109 .get(header::AUTHORIZATION)
110 .and_then(|v| v.to_str().ok())
111 .and_then(|v| v.strip_prefix("Bearer "))
112 .ok_or(Refused::Unauthorized("a bearer token is required"))?;
113 let claims = verifier
114 .verify(token.trim())
115 .map_err(|_| Refused::Unauthorized("invalid or expired token"))?;
116 tracing::Span::current().record("campaign", tracing::field::display(claims.campaign));
118 if claims.settings.read_only && !parts.method.is_safe() {
119 return Err(Refused::ReadOnly);
120 }
121 Ok(Authenticated(claims))
122 }
123}
124
125#[derive(Debug, PartialEq, Eq)]
127pub enum Refused {
128 Unauthorized(&'static str),
130 ReadOnly,
132}
133
134pub const READ_ONLY: &str = "this campaign is read-only for now; nothing in it can be changed";
136
137impl IntoResponse for Refused {
138 fn into_response(self) -> Response {
139 match self {
140 Refused::Unauthorized(why) => (
141 StatusCode::UNAUTHORIZED,
142 [(header::WWW_AUTHENTICATE, "Bearer")],
143 Json(serde_json::json!({ "error": why })),
144 )
145 .into_response(),
146 Refused::ReadOnly => (
147 StatusCode::FORBIDDEN,
148 Json(serde_json::json!({ "error": READ_ONLY })),
149 )
150 .into_response(),
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use chrono::Utc;
158 use uuid::Uuid;
159
160 use super::*;
161 use crate::protocol::Principal;
162
163 fn claims(lifetime: chrono::Duration) -> Claims {
164 Claims {
165 key: DeviceKey::generate().public(),
166 campaign: Uuid::new_v4(),
167 role: Principal::Player,
168 name: "Vex's laptop".into(),
169 edition: edda_types::Edition::default(),
170 settings: crate::protocol::CampaignSettings::default(),
171 expires_at: Utc::now() + lifetime,
172 }
173 }
174
175 #[test]
176 fn tokens_verify_only_with_the_right_key_and_until_they_expire() {
177 let signer = TokenSigner::new(&DeviceKey::generate());
178 let verifier = TokenVerifier::new(signer.public_key());
179 let claims = claims(chrono::Duration::minutes(15));
180 let token = signer.issue(&claims).unwrap();
181 assert!(token.starts_with("v4.public."));
182 assert_eq!(verifier.verify(&token), Ok(claims.clone()));
183
184 let stranger = TokenVerifier::new(DeviceKey::generate().public());
185 assert_eq!(stranger.verify(&token), Err(AuthError::InvalidToken));
186
187 let mut tampered = token.clone();
188 let last = tampered.pop().unwrap();
189 tampered.push(if last == 'A' { 'B' } else { 'A' });
190 assert_eq!(verifier.verify(&tampered), Err(AuthError::InvalidToken));
191
192 let expired = signer
193 .issue(&self::claims(chrono::Duration::seconds(-5)))
194 .unwrap();
195 assert_eq!(verifier.verify(&expired), Err(AuthError::InvalidToken));
196 }
197}