opentalk_service_auth/
api_key.rs1use std::{str::FromStr, time::Duration};
7
8use jsonwebtoken::{Algorithm, EncodingKey, Header, encode, get_current_timestamp};
9use rand::{RngExt, distr::Alphanumeric, rng};
10use serde::{Deserialize, Serialize};
11
12use crate::{ApiKeyId, ApiKeySecret};
13
14pub const JWT_EXPIRY: Duration = Duration::from_secs(60);
15
16#[derive(Debug, Clone, thiserror::Error)]
17#[error("Failed to encode JSON web token: {0:?}")]
18pub struct EncodingError(#[from] jsonwebtoken::errors::Error);
19
20#[derive(Serialize)]
22struct ExpiryClaims {
23 exp: u64,
25 iat: u64,
27}
28
29#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
31pub struct ApiKey {
32 pub id: ApiKeyId,
34 pub secret: ApiKeySecret,
36}
37
38impl ApiKey {
39 pub fn new<I, K>(id: I, secret: K) -> Self
41 where
42 I: Into<ApiKeyId>,
43 K: Into<ApiKeySecret>,
44 {
45 Self {
46 id: id.into(),
47 secret: secret.into(),
48 }
49 }
50
51 pub fn generate_jwt(&self) -> Result<String, EncodingError> {
55 let iat = get_current_timestamp();
56 let exp = iat + JWT_EXPIRY.as_secs();
57
58 let mut header = Header::new(Algorithm::HS256);
59
60 let nonce = rng()
61 .sample_iter(Alphanumeric)
62 .take(16)
63 .map(|ascii_byte| ascii_byte as char)
64 .collect();
65
66 header.kid = Some(self.id.0.clone());
67 header.nonce = Some(nonce);
68
69 Ok(encode(
70 &header,
71 &ExpiryClaims { exp, iat },
72 &EncodingKey::from_secret(self.secret.0.as_bytes()),
73 )?)
74 }
75}
76
77#[derive(Debug, thiserror::Error)]
78pub enum ParseApiKeyError {
79 #[error("missing colon delimiter")]
80 MissingColon,
81 #[error("key id must not be empty")]
82 EmptyKeyId,
83 #[error("key secret must not be empty")]
84 EmptyKeySecret,
85}
86
87impl FromStr for ApiKey {
88 type Err = ParseApiKeyError;
89
90 fn from_str(s: &str) -> Result<Self, Self::Err> {
91 match s.split_once(':') {
92 Some((key_id, secret)) => {
93 if key_id.is_empty() {
94 return Err(ParseApiKeyError::EmptyKeyId);
95 }
96
97 if secret.is_empty() {
98 return Err(ParseApiKeyError::EmptyKeySecret);
99 }
100
101 Ok(ApiKey::new(key_id, secret))
102 }
103 None => Err(ParseApiKeyError::MissingColon),
104 }
105 }
106}
107
108impl<'de> serde::Deserialize<'de> for ApiKey {
109 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110 where
111 D: serde::Deserializer<'de>,
112 {
113 #[derive(Deserialize)]
114 #[serde(untagged)]
115 enum ApiKeyDeserializeHelper {
116 String(String),
117 Struct { id: ApiKeyId, secret: ApiKeySecret },
118 }
119
120 match ApiKeyDeserializeHelper::deserialize(deserializer).map_err(|_| {
121 serde::de::Error::custom(
122 "\
123 Failed to deserialize API key\n\
124 Expected structure with fields 'id' and 'secret' or string with format '<id>:<secret>'\
125 ",
126 )
127 })? {
128 ApiKeyDeserializeHelper::String(s) => ApiKey::from_str(&s)
129 .map_err(|e| serde::de::Error::custom(e.to_string())),
130 ApiKeyDeserializeHelper::Struct { id, secret } => {
131 if id.0.is_empty() {
132 return Err(<D::Error as serde::de::Error>::custom(ParseApiKeyError::EmptyKeyId.to_string()));
133 }
134
135 if secret.0.is_empty() {
136 return Err(<D::Error as serde::de::Error>::custom(ParseApiKeyError::EmptyKeySecret.to_string()));
137 }
138
139 Ok(ApiKey { id, secret })
140 }
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use serde::Deserialize;
148
149 use crate::api_key::ApiKey;
150
151 #[derive(Deserialize)]
152 struct Test {
153 api_key: ApiKey,
154 }
155
156 #[test]
157 fn deserialize_str() {
158 let toml = r#"
159 api_key = "roomserver:secret"
160 "#;
161
162 let Test { api_key } = match toml::from_str(toml) {
163 Ok(c) => c,
164 Err(e) => panic!("{e}"),
165 };
166
167 assert_eq!(api_key, ApiKey::new("roomserver", "secret"))
168 }
169
170 #[test]
171 fn deserialize_missing_key_str() {
172 let toml = r#"
173 api_key = ":secret"
174 "#;
175
176 if toml::from_str::<Test>(toml).is_ok() {
177 panic!("Expected error")
178 };
179 }
180
181 #[test]
182 fn deserialize_missing_secret_str() {
183 let toml = r#"
184 api_key = "key_id:"
185 "#;
186
187 if toml::from_str::<Test>(toml).is_ok() {
188 panic!("Expected error")
189 };
190 }
191
192 #[test]
193 fn deserialize_missing_colon_str() {
194 let toml = r#"
195 api_key = "key_id:"
196 "#;
197
198 if toml::from_str::<Test>(toml).is_ok() {
199 panic!("Expected error")
200 };
201 }
202
203 #[test]
204 fn deserialize_struct() {
205 let toml = r#"
206 [api_key]
207 id = "roomserver"
208 secret = "secret"
209 "#;
210
211 let Test { api_key } = match toml::from_str(toml) {
212 Ok(c) => c,
213 Err(e) => panic!("{e}"),
214 };
215
216 assert_eq!(api_key, ApiKey::new("roomserver", "secret"))
217 }
218}