Browse Source

Many small changes to query the API & persist refresh tokens

main
Alex Feldman-Crough 4 weeks ago
parent
commit
6bb7e64d8e
  1. 2
      Cargo.toml
  2. 45
      src/api/mod.rs
  3. 11
      src/api/types.rs
  4. 79
      src/client/client.rs
  5. 2
      src/lib.rs

2
Cargo.toml

@ -34,7 +34,7 @@ features = ["serde"]
[dependencies.tokio]
version = "1.6"
features = ["signal", "time"]
features = ["fs"]
[dependencies.url]
version = "2.2"

45
src/api/mod.rs

@ -83,6 +83,40 @@ pub async fn refresh_token(
Ok(token)
}
pub async fn whoami(
people_api: &Url,
client: &http::Client,
token: &types::AccessToken,
) -> Result<String> {
#[derive(Deserialize)]
#[serde(rename_all="camelCase")]
struct Person {
email_addresses: Vec<Data<String>>,
}
#[derive(Deserialize)]
#[serde(rename_all="camelCase")]
struct Data<T> {
value: T,
}
let mut uri = people_api.join("/v1/people/me").expect("whoami url");
uri.query_pairs_mut()
.append_pair("personFields", "emailAddresses");
let (k, v) = token.auth_header();
let req = http::request()
.uri(uri.as_str())
.header(k, v)
.body(http::Body::default())
.expect("whoami request");
let resp = client.request(req).await?;
let person = parse_body::<Person>(resp).await?;
Ok(person.email_addresses.get(0)
.expect("no emails returned")
.value
.clone())
}
async fn api_call<I, O>(client: &http::Client, uri: &Url, body: I) -> Result<O>
where
I: Serialize,
@ -100,8 +134,15 @@ where
.body(http::Body::from(request_string))
.expect("malformed HTTP request");
let response = client.request(request).await?;
let status = response.status();
let body: Vec<u8> = response
parse_body(response).await
}
async fn parse_body<O>(resp: http::Response) -> Result<O>
where
O: for<'de> Deserialize<'de>,
{
let status = resp.status();
let body: Vec<u8> = resp
.into_body()
.map_ok(|bytes| {
let iter = bytes.into_iter().map(Ok::<_, hyper::Error>);

11
src/api/types.rs

@ -6,6 +6,7 @@ use derive_more::{From, Into, Deref};
use secret::Secret;
use chrono::{DateTime, Duration, Utc};
use std::sync::Arc;
use crate::http::header::{self, HeaderName, HeaderValue};
#[derive(Clone, Eq, Debug, Deserialize, Deref, From, Into, Hash, PartialEq,
Serialize)]
@ -111,11 +112,21 @@ pub type OneTimeToken = Token<()>;
#[derive(Clone, Eq, Debug, Deserialize, Deref, From, Into, Hash, PartialEq,
Serialize)]
#[deref(forward)]
pub struct AuthorizationCode(Secret<String>);
#[derive(Clone, Eq, Debug, Deref, From, Into, Hash, PartialEq)]
#[deref(forward)]
pub struct AccessToken(Arc<Secret<str>>);
impl AccessToken {
pub fn auth_header(&self) -> (HeaderName, HeaderValue) {
let value = format!("Bearer {}", &self as &str);
(header::AUTHORIZATION,
HeaderValue::from_str(&value).expect("token to header value"))
}
}
impl<'de> Deserialize<'de> for AccessToken {
fn deserialize<D>(de: D) -> Result<Self, D::Error>
where

79
src/client/client.rs

@ -3,6 +3,13 @@ use url::Url;
use std::mem;
use std::borrow::Cow;
use rand::Rng;
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
use tokio::io::AsyncReadExt;
use std::os::unix::fs::PermissionsExt;
use secret::Secret;
use std::sync::Arc;
use std::io::ErrorKind::NotFound;
use crate::api;
use crate::config;
@ -20,12 +27,58 @@ impl Client {
data: Data {
config,
default_redirect,
refresh_token_path: None,
http: http::new_client(),
notify: Notify::new(),
},
}
}
pub async fn new_persistent(config: config::OAuth2,
default_redirect: Url,
refresh_token_path: impl Into<PathBuf>)
-> Result<Self>
{
let http = http::new_client();
let refresh_token_path: PathBuf = refresh_token_path.into();
let state = match tokio::fs::File::open(&refresh_token_path).await {
Err(e) if e.kind() == NotFound => {
log::debug!("{} does not exist, cannot restore state",
refresh_token_path.display());
State::Initial
},
Err(e) => {
log::error!("failed to access {}: {}",
refresh_token_path.display(),
e);
return Err(e.into());
},
Ok(mut file) => {
let mut data = String::with_capacity(128);
file.read_to_string(&mut data).await?;
data.shrink_to_fit();
log::info!("granting token using persisted refresh token");
let refresh_token = api::RefreshToken::from(
Secret::from_arc(Arc::from(data))
);
let token = api::refresh_token(&config, &http,
&refresh_token).await?
.set_refresh_token(refresh_token);
State::Authorized(token)
},
};
Ok(Client {
state: Mutex::new(state),
data: Data {
config,
http,
default_redirect,
refresh_token_path: Some(refresh_token_path),
notify: Notify::new(),
},
})
}
pub async fn try_token(&self) -> Result<Option<api::AccessToken>> {
log::debug!("fetching access token from Client state");
let mut state = self.state.lock().await;
@ -37,6 +90,7 @@ impl Client {
},
State::Authorized(ref mut token) => {
if token.expired() {
log::debug!("token is expired; refreshing it");
let new_token = api::refresh_token(
&self.data.config,
&self.data.http,
@ -135,6 +189,7 @@ impl Client {
.refreshable()
.expect("OAuth2 is already configured to return \
refreshable tokens");
self.persist_refresh(token.refresh_token()).await?;
*lock = State::Authorized(token);
log::debug!("notifying threads waiting on a token");
self.data.notify.notify_waiters();
@ -142,13 +197,35 @@ impl Client {
}
}
}
async fn persist_refresh(&self, token: &api::RefreshToken) -> Result<()> {
if let Some(path) = self.data.refresh_token_path.as_deref() {
log::debug!("persisting refresh token to {}", path.display());
let mut file = tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.await?;
let mut permissions = file.metadata().await?.permissions();
permissions.set_mode(0o400);
file.set_permissions(permissions).await?;
file.write_all(token.as_bytes()).await?;
} else {
log::debug!("no persistance path set; not storing refresh token");
}
Ok(())
}
}
struct Data {
config: config::OAuth2,
default_redirect: Url,
notify: Notify,
http: http::Client,
refresh_token_path: Option<PathBuf>,
}
enum State {
@ -169,6 +246,8 @@ pub enum Error {
already authorized"
)]
AlreadyAuthorized,
#[error(transparent)]
IO(#[from] std::io::Error),
#[error("Authorization was not requested")]
NotInitialized,
#[error("The request had missing or malformed parameters")]

2
src/lib.rs

@ -1,6 +1,8 @@
pub use client::Client;
pub use api::types::{Token, RefreshToken, AccessToken, AuthorizationCode,
StateKey, RefreshableToken, OneTimeToken};
pub use http::new_client as new_https_client;
pub mod api;
pub mod config;
pub mod client;

Loading…
Cancel
Save