1use crate::type_emit::var;
8use serde_json::{json, Value};
9
10pub fn set_var(entry: &mut Value, key: &str, value: &str, secret: bool) {
11 let vars = entry
12 .as_object_mut()
13 .expect("entry is an object")
14 .entry("extra_vars")
15 .or_insert_with(|| json!([]));
16 let vars = vars.as_array_mut().expect("extra_vars is an array");
17 match vars
18 .iter_mut()
19 .find(|v| v.get("key").and_then(Value::as_str) == Some(key))
20 {
21 Some(slot) => slot["value"] = json!(value),
22 None => vars.push(json!({ "key": key, "value": value, "secret": secret })),
23 }
24}
25
26pub fn refresh_request(entry: &Value) -> Result<(String, Vec<(String, String)>), String> {
30 let url = var(entry, "token_url").to_string();
31 if !(url.starts_with("https://")
32 || url.starts_with("http://127.0.0.1")
33 || url.starts_with("http://localhost"))
34 {
35 return Err("token_url must be https:// (plain http is allowed for localhost only)".into());
36 }
37 let refresh = var(entry, "refresh_token");
38 if refresh.is_empty() {
39 return Err("the entry holds no refresh_token".into());
40 }
41 let mut form = vec![
42 ("grant_type".to_string(), "refresh_token".to_string()),
43 ("refresh_token".to_string(), refresh.to_string()),
44 ("client_id".to_string(), var(entry, "client_id").to_string()),
45 ];
46 for (field, name) in [("client_secret", "client_secret"), ("scopes", "scope")] {
47 let v = var(entry, field);
48 if !v.is_empty() {
49 form.push((name.to_string(), v.to_string()));
50 }
51 }
52 Ok((url, form))
53}
54
55pub fn refresh_host(url: &str) -> &str {
57 url.split("://")
58 .nth(1)
59 .and_then(|r| r.split('/').next())
60 .unwrap_or("")
61}
62
63#[derive(Debug, PartialEq)]
65pub struct Grant {
66 pub access_token: String,
67 pub refresh_token: Option<String>,
68 pub expires_in: Option<i64>,
69}
70
71pub fn parse_grant(body: &Value) -> Result<Grant, String> {
72 if let Some(err) = body.get("error").and_then(Value::as_str) {
73 return Err(format!("the issuer refused the refresh: {err}"));
75 }
76 let access = body
77 .get("access_token")
78 .and_then(Value::as_str)
79 .filter(|s| !s.is_empty())
80 .ok_or("the response has no access_token")?;
81 Ok(Grant {
82 access_token: access.to_string(),
83 refresh_token: body
84 .get("refresh_token")
85 .and_then(Value::as_str)
86 .filter(|s| !s.is_empty())
87 .map(String::from),
88 expires_in: body.get("expires_in").and_then(Value::as_i64),
89 })
90}
91
92pub fn apply_grant(entry: &mut Value, g: &Grant, now_unix: i64) -> bool {
94 let rotated = g
95 .refresh_token
96 .as_deref()
97 .is_some_and(|r| r != var(entry, "refresh_token"));
98 if let Some(r) = &g.refresh_token {
99 set_var(entry, "refresh_token", r, true);
100 }
101 set_var(entry, "access_token", &g.access_token, true);
102 if let Some(secs) = g.expires_in {
103 let at = crate::pool::iso_at(now_unix + secs);
104 set_var(entry, "access_expires_at", &at, false);
105 }
106 rotated
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::type_emit::var;
113
114 fn entry() -> Value {
115 json!({"secretType":"oauth_client","provider":"Slack","extra_vars":[
116 {"key":"refresh_token","value":"old-r","secret":true}]})
117 }
118
119 #[test]
120 fn a_rotated_refresh_token_replaces_the_old_one_and_is_reported() {
121 let g =
122 parse_grant(&json!({"access_token":"a1","refresh_token":"new-r","expires_in":3600}))
123 .unwrap();
124 let mut e = entry();
125 assert!(apply_grant(&mut e, &g, 1_700_000_000));
126 assert_eq!(var(&e, "refresh_token"), "new-r");
127 assert_eq!(var(&e, "access_token"), "a1");
128 assert_eq!(var(&e, "access_expires_at"), "2023-11-14T23:13:20Z");
129 }
130
131 #[test]
132 fn an_unrotated_response_keeps_the_stored_refresh_token() {
133 let g = parse_grant(&json!({"access_token":"a2"})).unwrap();
134 let mut e = entry();
135 assert!(!apply_grant(&mut e, &g, 0));
136 assert_eq!(var(&e, "refresh_token"), "old-r");
137 assert_eq!(var(&e, "access_expires_at"), "");
138 }
139
140 #[test]
141 fn issuer_errors_are_named_without_echoing_the_body() {
142 let err = parse_grant(
143 &json!({"error":"invalid_grant","error_description":"secret-bearing text"}),
144 )
145 .unwrap_err();
146 assert!(err.contains("invalid_grant") && !err.contains("secret-bearing"));
147 assert!(parse_grant(&json!({})).is_err());
148 }
149}