use tokio::sync::Mutex;
use super::oauth;
use super::token::{OAuthTokens, TokenStore};
use crate::api::client::{BoxFuture, Credentials};
use crate::api::error::ApiError;
pub struct OAuthSession {
store: TokenStore,
tokens: Mutex<OAuthTokens>,
}
impl OAuthSession {
pub fn new(store: TokenStore, tokens: OAuthTokens) -> Self {
Self {
store,
tokens: Mutex::new(tokens),
}
}
async fn renew(&self, tokens: &mut OAuthTokens) -> Result<(), ApiError> {
let fresh = oauth::refresh_token(&tokens.refresh_token)
.await
.map_err(|e| ApiError::Refresh(format!("{e:#}")))?;
if let Err(e) = self.store.save(&fresh) {
tracing::warn!(error = %e, "could not store the refreshed token");
}
tracing::info!("OAuth token refreshed");
*tokens = fresh;
Ok(())
}
}
fn bearer(tokens: &OAuthTokens) -> String {
format!("Bearer {}", tokens.access_token)
}
impl Credentials for OAuthSession {
fn authorization(&self) -> BoxFuture<'_, Result<String, ApiError>> {
Box::pin(async move {
let mut tokens = self.tokens.lock().await;
if tokens.is_expired() && !tokens.refresh_token.is_empty() {
self.renew(&mut tokens).await?;
}
Ok(bearer(&tokens))
})
}
fn refresh<'a>(&'a self, rejected: &'a str) -> BoxFuture<'a, Result<bool, ApiError>> {
Box::pin(async move {
let mut tokens = self.tokens.lock().await;
if bearer(&tokens) != rejected {
return Ok(true);
}
if tokens.refresh_token.is_empty() {
return Ok(false);
}
self.renew(&mut tokens).await?;
Ok(true)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn session(access: &str, refresh: &str) -> OAuthSession {
let store = TokenStore::at(std::env::temp_dir().join(format!(
"linear-tui-session-{}-{access}.json",
std::process::id()
)));
OAuthSession::new(
store,
OAuthTokens {
access_token: access.into(),
refresh_token: refresh.into(),
expires_at: u64::MAX / 2,
},
)
}
#[tokio::test]
async fn a_live_token_is_sent_as_a_bearer_header() {
assert_eq!(
session("a1", "r1").authorization().await.unwrap(),
"Bearer a1"
);
}
#[tokio::test]
async fn a_token_someone_else_already_renewed_is_not_renewed_again() {
assert!(session("a2", "r1").refresh("Bearer a1").await.unwrap());
}
#[tokio::test]
async fn without_a_refresh_token_there_is_nothing_to_retry_with() {
assert!(!session("a1", "").refresh("Bearer a1").await.unwrap());
}
}