cred: add pure scripts, signing, and OAuth refresh; validation blocked
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
//! OAuth2 refresh exchange and a process-local cache keyed by item and version.
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Error;
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("OAuth refresh failed or returned an invalid response")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
/// HTTP adapter. Implementations must reject unsuccessful HTTP status codes.
|
||||
pub trait Http: Sync {
|
||||
fn send(
|
||||
&self,
|
||||
request: &Request,
|
||||
deadline: Instant,
|
||||
) -> impl std::future::Future<Output = Result<Zeroizing<Vec<u8>>, Error>> + Send;
|
||||
}
|
||||
/// Contains credentials. Deliberately does not implement Debug or Serialize.
|
||||
pub struct Request {
|
||||
pub url: String,
|
||||
pub method: &'static str,
|
||||
pub headers: Vec<(&'static str, String)>,
|
||||
pub body: Zeroizing<String>,
|
||||
}
|
||||
pub fn request(
|
||||
token_url: &str,
|
||||
refresh_token: &str,
|
||||
client_id: &str,
|
||||
client_secret: &str,
|
||||
) -> Result<Request, Error> {
|
||||
let target = url::Url::parse(token_url).map_err(|_| Error)?;
|
||||
if target.scheme() != "https"
|
||||
|| !target.username().is_empty()
|
||||
|| target.password().is_some()
|
||||
|| target.fragment().is_some()
|
||||
|| [refresh_token, client_id, client_secret].contains(&"")
|
||||
{
|
||||
return Err(Error);
|
||||
}
|
||||
let body = url::form_urlencoded::Serializer::new(String::new())
|
||||
.append_pair("grant_type", "refresh_token")
|
||||
.append_pair("refresh_token", refresh_token)
|
||||
.append_pair("client_id", client_id)
|
||||
.append_pair("client_secret", client_secret)
|
||||
.finish();
|
||||
Ok(Request {
|
||||
url: token_url.into(),
|
||||
method: "POST",
|
||||
headers: vec![
|
||||
("Content-Type", "application/x-www-form-urlencoded".into()),
|
||||
("Content-Length", body.len().to_string()),
|
||||
],
|
||||
body: Zeroizing::new(body),
|
||||
})
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct Token {
|
||||
pub value: Zeroizing<String>,
|
||||
pub scopes: Option<Vec<String>>,
|
||||
pub expires: Instant,
|
||||
}
|
||||
pub fn decode(bytes: &[u8]) -> Result<Token, Error> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Response {
|
||||
access_token: String,
|
||||
token_type: String,
|
||||
expires_in: u64,
|
||||
scope: Option<String>,
|
||||
}
|
||||
let r: Response = serde_json::from_slice(bytes).map_err(|_| Error)?;
|
||||
if r.access_token.is_empty()
|
||||
|| !r.token_type.eq_ignore_ascii_case("bearer")
|
||||
|| r.expires_in == 0
|
||||
|| http::header::HeaderValue::from_str(&format!("Bearer {}", r.access_token)).is_err()
|
||||
{
|
||||
return Err(Error);
|
||||
}
|
||||
Ok(Token {
|
||||
value: Zeroizing::new(r.access_token),
|
||||
scopes: r
|
||||
.scope
|
||||
.map(|s| s.split_whitespace().map(str::to_owned).collect()),
|
||||
expires: Instant::now() + Duration::from_secs(r.expires_in.min(86400).saturating_sub(30)),
|
||||
})
|
||||
}
|
||||
/// The caller owns synchronization and endpoint policy. No lock crosses an HTTP call here.
|
||||
#[derive(Default)]
|
||||
pub struct Cache {
|
||||
tokens: BTreeMap<(String, i64), Token>,
|
||||
}
|
||||
impl Cache {
|
||||
pub fn clear(&mut self) {
|
||||
self.tokens.clear();
|
||||
}
|
||||
pub async fn refresh(
|
||||
&mut self,
|
||||
item: &str,
|
||||
version: i64,
|
||||
request: &Request,
|
||||
http: &impl Http,
|
||||
deadline: Instant,
|
||||
) -> Result<Token, Error> {
|
||||
let key = (item.to_owned(), version);
|
||||
if let Some(t) = self.tokens.get(&key).filter(|t| t.expires > Instant::now()) {
|
||||
return Ok(t.clone());
|
||||
}
|
||||
self.tokens.retain(|(id, _), _| id != item);
|
||||
let token = decode(&http.send(request, deadline).await?)?;
|
||||
self.tokens.insert(key, token.clone());
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user