1use base64::Engine;
6use serde_json::Value;
7
8use crate::consts::{CLERK_BASE_URL, CLERK_JS_VERSION, CLERK_TOKEN_JS_VERSION, JWT_REFRESH_BUFFER};
9use crate::error::{Error, Result};
10use crate::http::{Http, HttpRequest, Method};
11
12pub(crate) fn normalise_token(token: &str) -> String {
17 let token = token.trim();
18 if token.starts_with("eyJ") {
19 return format!("__client={token}");
20 }
21 if token.contains("__client=") {
22 for part in token.split(';') {
23 if let Some(value) = part.trim().strip_prefix("__client=") {
24 return format!("__client={value}");
25 }
26 }
27 }
28 format!("__client={token}")
29}
30
31pub(crate) fn decode_jwt_exp(token: &str) -> i64 {
35 let Some(payload) = token.split('.').nth(1) else {
36 return 0;
37 };
38 let payload = payload.trim_end_matches('=');
39 let Ok(bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload) else {
40 return 0;
41 };
42 let Ok(value) = serde_json::from_slice::<Value>(&bytes) else {
43 return 0;
44 };
45 value.get("exp").and_then(Value::as_i64).unwrap_or(0)
46}
47
48struct ClientInfo {
49 session_id: String,
50 user_id: Option<String>,
51 display_name: Option<String>,
52}
53
54fn parse_client_response(data: &Value) -> Result<ClientInfo> {
55 let response = data
56 .get("response")
57 .filter(|value| !value.is_null())
58 .ok_or_else(|| Error::Auth("invalid Clerk response; the cookie may be expired".into()))?;
59
60 let session_id = response
61 .get("last_active_session_id")
62 .and_then(Value::as_str)
63 .filter(|id| !id.is_empty())
64 .ok_or_else(|| Error::Auth("no active session; the cookie may be expired".into()))?
65 .to_string();
66
67 let mut user_id = None;
68 let mut display_name = None;
69 if let Some(sessions) = response.get("sessions").and_then(Value::as_array) {
70 for session in sessions {
71 if session.get("id").and_then(Value::as_str) == Some(session_id.as_str()) {
72 let user = session.get("user").cloned().unwrap_or(Value::Null);
73 user_id = user.get("id").and_then(Value::as_str).map(str::to_string);
74 display_name = derive_display_name(&user);
75 break;
76 }
77 }
78 }
79 Ok(ClientInfo {
80 session_id,
81 user_id,
82 display_name,
83 })
84}
85
86fn derive_display_name(user: &Value) -> Option<String> {
89 let field = |key: &str| {
90 user.get(key)
91 .and_then(Value::as_str)
92 .unwrap_or("")
93 .trim()
94 .to_string()
95 };
96 let first = field("first_name");
97 let last = field("last_name");
98 let username = field("username");
99
100 if !username.is_empty() && !username.contains('@') {
101 Some(username)
102 } else if !first.is_empty() && !first.contains('@') {
103 Some(if last.is_empty() {
104 first
105 } else {
106 format!("{first} {last}")
107 })
108 } else if !username.is_empty() && username.contains('@') {
109 let local: String = username
110 .split('@')
111 .next()
112 .unwrap_or("")
113 .trim()
114 .chars()
115 .take(100)
116 .collect();
117 (!local.is_empty()).then_some(local)
118 } else {
119 None
120 }
121}
122
123fn parse_token_response(data: &Value) -> Result<String> {
124 data.get("jwt")
125 .and_then(Value::as_str)
126 .filter(|jwt| !jwt.is_empty())
127 .map(str::to_string)
128 .ok_or_else(|| Error::Auth("no JWT in the Clerk token response".into()))
129}
130
131pub struct ClerkAuth {
133 cookie: String,
134 jwt: Option<String>,
135 jwt_exp: i64,
136 session_id: Option<String>,
137 user_id: Option<String>,
138 display_name: Option<String>,
139}
140
141impl ClerkAuth {
142 pub fn new(token: &str) -> Self {
144 Self {
145 cookie: normalise_token(token),
146 jwt: None,
147 jwt_exp: 0,
148 session_id: None,
149 user_id: None,
150 display_name: None,
151 }
152 }
153
154 pub fn user_id(&self) -> Option<&str> {
156 self.user_id.as_deref()
157 }
158
159 pub fn display_name(&self) -> &str {
161 self.display_name.as_deref().unwrap_or("Suno")
162 }
163
164 pub async fn authenticate(&mut self, http: &impl Http) -> Result<String> {
166 self.fetch_session(http).await?;
167 self.refresh_jwt(http).await?;
168 self.user_id.clone().ok_or_else(|| {
169 Error::Auth("could not determine the user ID from the Clerk session".into())
170 })
171 }
172
173 pub async fn ensure_jwt(&mut self, http: &impl Http) -> Result<String> {
175 if self.jwt.is_none() || now_unix() >= self.jwt_exp - JWT_REFRESH_BUFFER {
176 self.refresh_jwt(http).await?;
177 }
178 self.jwt
179 .clone()
180 .ok_or_else(|| Error::Auth("failed to obtain a JWT".into()))
181 }
182
183 pub fn invalidate_jwt(&mut self) {
185 self.jwt = None;
186 }
187
188 async fn fetch_session(&mut self, http: &impl Http) -> Result<()> {
189 let cookie = self.cookie.clone();
190 let url = format!("{CLERK_BASE_URL}/v1/client?_clerk_js_version={CLERK_JS_VERSION}");
191 let data = clerk_request_json(http, &cookie, Method::Get, url).await?;
192 let info = parse_client_response(&data)?;
193 self.session_id = Some(info.session_id);
194 self.user_id = info.user_id;
195 self.display_name = info.display_name;
196 Ok(())
197 }
198
199 async fn refresh_jwt(&mut self, http: &impl Http) -> Result<()> {
200 if self.session_id.is_none() {
201 self.fetch_session(http).await?;
202 }
203 let session_id = self
204 .session_id
205 .clone()
206 .ok_or_else(|| Error::Auth("no Clerk session".into()))?;
207 let cookie = self.cookie.clone();
208 let url = format!(
209 "{CLERK_BASE_URL}/v1/client/sessions/{session_id}/tokens?_clerk_js_version={CLERK_TOKEN_JS_VERSION}"
210 );
211 let data = clerk_request_json(http, &cookie, Method::Post, url).await?;
212 let jwt = parse_token_response(&data)?;
213 self.jwt_exp = decode_jwt_exp(&jwt);
214 self.jwt = Some(jwt);
215 Ok(())
216 }
217}
218
219async fn clerk_request_json(
220 http: &impl Http,
221 cookie: &str,
222 method: Method,
223 url: String,
224) -> Result<Value> {
225 let request = HttpRequest {
226 method,
227 url,
228 headers: vec![("Cookie".to_string(), cookie.to_string())],
229 };
230 let response = http
231 .send(request)
232 .await
233 .map_err(|err| Error::Connection(format!("could not connect to Clerk: {err}")))?;
234 if response.status != 200 {
235 return Err(Error::Auth(format!(
236 "Clerk request failed with status {}",
237 response.status
238 )));
239 }
240 serde_json::from_slice(&response.body)
241 .map_err(|err| Error::Connection(format!("invalid Clerk response: {err}")))
242}
243
244fn now_unix() -> i64 {
245 use std::time::{SystemTime, UNIX_EPOCH};
246 SystemTime::now()
247 .duration_since(UNIX_EPOCH)
248 .map(|d| d.as_secs() as i64)
249 .unwrap_or(0)
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::testutil::{MockHttp, Rule};
256
257 fn jwt_with_exp(exp: i64) -> String {
258 let payload =
259 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!("{{\"exp\":{exp}}}"));
260 format!("eyJhbGciOiJIUzI1NiJ9.{payload}.signature")
261 }
262
263 #[test]
264 fn normalise_accepts_raw_jwt() {
265 assert_eq!(normalise_token(" eyJabc "), "__client=eyJabc");
266 }
267
268 #[test]
269 fn normalise_extracts_from_cookie_header() {
270 assert_eq!(
271 normalise_token("foo=1; __client=eyJabc; bar=2"),
272 "__client=eyJabc"
273 );
274 }
275
276 #[test]
277 fn normalise_wraps_unknown_value() {
278 assert_eq!(normalise_token("rawvalue"), "__client=rawvalue");
279 }
280
281 #[test]
282 fn decode_exp_reads_claim() {
283 assert_eq!(decode_jwt_exp(&jwt_with_exp(1_893_456_000)), 1_893_456_000);
284 }
285
286 #[test]
287 fn decode_exp_handles_garbage() {
288 assert_eq!(decode_jwt_exp("not-a-jwt"), 0);
289 assert_eq!(decode_jwt_exp(""), 0);
290 }
291
292 #[test]
293 fn display_name_prefers_username() {
294 let user = serde_json::json!({"username": "teh-hippo", "first_name": "Ignored"});
295 assert_eq!(derive_display_name(&user).as_deref(), Some("teh-hippo"));
296 }
297
298 #[test]
299 fn display_name_uses_first_last_when_no_username() {
300 let user = serde_json::json!({"first_name": "Ada", "last_name": "Lovelace"});
301 assert_eq!(derive_display_name(&user).as_deref(), Some("Ada Lovelace"));
302 }
303
304 #[test]
305 fn display_name_falls_back_to_email_local_part() {
306 let user = serde_json::json!({"username": "yshvq8dp9v@privaterelay.appleid.com"});
307 assert_eq!(derive_display_name(&user).as_deref(), Some("yshvq8dp9v"));
308 }
309
310 #[test]
311 fn parse_client_requires_a_session() {
312 let data = serde_json::json!({"response": {"sessions": []}});
313 assert!(parse_client_response(&data).is_err());
314 }
315
316 #[test]
317 fn authenticate_fetches_user_and_jwt() {
318 let client_body = serde_json::json!({
319 "response": {
320 "last_active_session_id": "sess_1",
321 "sessions": [
322 {"id": "sess_1", "user": {"id": "user_1", "username": "teh-hippo"}}
323 ]
324 }
325 })
326 .to_string();
327 let token_body = serde_json::json!({"jwt": jwt_with_exp(1_893_456_000)}).to_string();
328
329 let http = MockHttp::new(vec![
331 Rule::new("/v1/client/sessions/", 200, token_body),
332 Rule::new("/v1/client", 200, client_body),
333 ]);
334
335 let mut auth = ClerkAuth::new("eyJtoken");
336 let user_id = pollster::block_on(auth.authenticate(&http)).unwrap();
337 assert_eq!(user_id, "user_1");
338 assert_eq!(auth.display_name(), "teh-hippo");
339
340 let jwt = pollster::block_on(auth.ensure_jwt(&http)).unwrap();
341 assert!(jwt.starts_with("eyJ"));
342 }
343}