Skip to main content

nils_common/provider_runtime/
auth.rs

1use std::path::Path;
2
3use serde_json::Value;
4
5use super::error::CoreError;
6use super::json;
7use super::jwt;
8
9pub fn identity_from_auth_file(path: &Path) -> Result<Option<String>, CoreError> {
10    let value = json::read_json(path)?;
11    let payload = decoded_payload_from_auth_json(&value);
12    Ok(identity_from_auth_json(payload.as_ref()))
13}
14
15pub fn email_from_auth_file(path: &Path) -> Result<Option<String>, CoreError> {
16    let value = json::read_json(path)?;
17    let payload = decoded_payload_from_auth_json(&value);
18    Ok(email_from_auth_json(payload.as_ref()))
19}
20
21pub fn account_id_from_auth_file(path: &Path) -> Result<Option<String>, CoreError> {
22    let value = json::read_json(path)?;
23    let payload = decoded_payload_from_auth_json(&value);
24    Ok(account_id_from_auth_json(&value, payload.as_ref()))
25}
26
27pub fn last_refresh_from_auth_file(path: &Path) -> Result<Option<String>, CoreError> {
28    let value = json::read_json(path)?;
29    Ok(json::string_at(&value, &["last_refresh"]))
30}
31
32pub fn identity_key_from_auth_file(path: &Path) -> Result<Option<String>, CoreError> {
33    let value = json::read_json(path)?;
34    let payload = decoded_payload_from_auth_json(&value);
35    let identity = identity_from_auth_json(payload.as_ref());
36    let identity = match identity {
37        Some(value) => value,
38        None => return Ok(None),
39    };
40    let account_id = account_id_from_auth_json(&value, payload.as_ref());
41    let key = match account_id {
42        Some(account) => format!("{}::{}", identity, account),
43        None => identity,
44    };
45    Ok(Some(key))
46}
47
48pub fn token_from_auth_json(value: &serde_json::Value) -> Option<String> {
49    json::string_at(value, &["tokens", "id_token"])
50        .or_else(|| json::string_at(value, &["id_token"]))
51        .or_else(|| json::string_at(value, &["tokens", "access_token"]))
52        .or_else(|| json::string_at(value, &["access_token"]))
53}
54
55fn decoded_payload_from_auth_json(value: &Value) -> Option<Value> {
56    let token = token_from_auth_json(value)?;
57    jwt::decode_payload_json(&token)
58}
59
60fn identity_from_auth_json(payload: Option<&Value>) -> Option<String> {
61    payload.and_then(jwt::identity_from_payload)
62}
63
64fn email_from_auth_json(payload: Option<&Value>) -> Option<String> {
65    payload.and_then(jwt::email_from_payload)
66}
67
68fn account_id_from_auth_json(value: &Value, payload: Option<&Value>) -> Option<String> {
69    json::string_at(value, &["tokens", "account_id"])
70        .or_else(|| json::string_at(value, &["account_id"]))
71        .or_else(|| {
72            payload
73                .and_then(|decoded| decoded.get("sub"))
74                .and_then(|sub| sub.as_str())
75                .map(json::strip_newlines)
76        })
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use base64::Engine;
83    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
84    use std::fs;
85    use tempfile::TempDir;
86
87    fn write_auth_json(path: &Path, contents: &str) {
88        fs::write(path, contents).expect("write auth json");
89    }
90
91    fn jwt(payload: Value) -> String {
92        let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
93        let payload = URL_SAFE_NO_PAD.encode(payload.to_string());
94        format!("{header}.{payload}.sig")
95    }
96
97    #[test]
98    fn account_id_falls_back_to_token_sub_when_fields_missing() {
99        let dir = TempDir::new().expect("tempdir");
100        let path = dir.path().join("auth.json");
101        let token = jwt(serde_json::json!({"sub":"acct_123"}));
102
103        write_auth_json(&path, &format!(r#"{{"tokens":{{"id_token":"{token}"}}}}"#));
104
105        assert_eq!(
106            account_id_from_auth_file(&path).expect("account id"),
107            Some("acct_123".to_string())
108        );
109    }
110
111    #[test]
112    fn identity_key_prefers_account_id_from_json_over_jwt_subject() {
113        let dir = TempDir::new().expect("tempdir");
114        let path = dir.path().join("auth.json");
115        let token = jwt(serde_json::json!({
116            "sub": "sub_from_jwt",
117            "https://api.openai.com/auth": {"chatgpt_user_id":"user_456"}
118        }));
119
120        write_auth_json(
121            &path,
122            &format!(r#"{{"tokens":{{"id_token":"{token}","account_id":"acct_999"}}}}"#),
123        );
124
125        assert_eq!(
126            identity_key_from_auth_file(&path).expect("identity key"),
127            Some("user_456::acct_999".to_string())
128        );
129    }
130
131    #[test]
132    fn identity_key_is_none_when_identity_is_missing() {
133        let dir = TempDir::new().expect("tempdir");
134        let path = dir.path().join("auth.json");
135        write_auth_json(&path, r#"{"tokens":{"account_id":"acct_100"}}"#);
136
137        assert_eq!(
138            identity_key_from_auth_file(&path).expect("identity key"),
139            None
140        );
141    }
142}