use log::info;
use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize};
use std::time;
const MACHINE_MAN_PREVIEW: &'static str =
"application/vnd.github.machine-man-preview+json";
#[derive(Debug)]
pub enum AuthError {
JwtError(jsonwebtoken::errors::Error),
InvalidHeaderValue(http::header::InvalidHeaderValue),
ReqwestError(reqwest::Error),
TimeError(time::SystemTimeError),
}
#[derive(Debug, Serialize)]
struct JwtClaims {
iat: u64,
exp: u64,
iss: u64,
}
impl JwtClaims {
fn new(params: &GithubAuthParams) -> Result<JwtClaims, AuthError> {
let now = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.map_err(AuthError::TimeError)?
.as_secs();
Ok(JwtClaims {
iat: now,
exp: now + 60,
iss: params.app_id,
})
}
}
#[derive(Debug, Deserialize)]
struct RawInstallationToken {
token: String,
}
fn get_installation_token(
client: &reqwest::Client,
params: &GithubAuthParams,
) -> Result<RawInstallationToken, AuthError> {
let claims = JwtClaims::new(params)?;
let mut header = jsonwebtoken::Header::default();
header.alg = jsonwebtoken::Algorithm::RS256;
let token = jsonwebtoken::encode(&header, &claims, ¶ms.private_key)
.map_err(AuthError::JwtError)?;
let url = format!(
"https://api.github.com/app/installations/{}/access_tokens",
params.installation_id
);
Ok(client
.post(&url)
.bearer_auth(token)
.header("Accept", MACHINE_MAN_PREVIEW)
.send()
.map_err(AuthError::ReqwestError)?
.error_for_status()
.map_err(AuthError::ReqwestError)?
.json()
.map_err(AuthError::ReqwestError)?)
}
pub struct InstallationToken {
pub client: reqwest::Client,
token: String,
fetch_time: time::SystemTime,
params: GithubAuthParams,
}
impl InstallationToken {
pub fn new(
params: GithubAuthParams,
) -> Result<InstallationToken, AuthError> {
let client = reqwest::Client::new();
let raw = get_installation_token(&client, ¶ms)?;
Ok(InstallationToken {
client,
token: raw.token,
fetch_time: time::SystemTime::now(),
params: params.clone(),
})
}
pub fn header(&mut self) -> Result<HeaderMap, AuthError> {
self.refresh()?;
let mut headers = HeaderMap::new();
let val = format!("token {}", self.token);
headers.insert(
"Authorization",
val.parse().map_err(AuthError::InvalidHeaderValue)?,
);
Ok(headers)
}
fn refresh(&mut self) -> Result<(), AuthError> {
let elapsed = time::SystemTime::now()
.duration_since(self.fetch_time)
.map_err(AuthError::TimeError)?;
if elapsed.as_secs() > (55 * 60) {
info!("refreshing installation token");
let raw = get_installation_token(&self.client, &self.params)?;
self.token = raw.token;
self.fetch_time = time::SystemTime::now();
}
Ok(())
}
}
#[derive(Clone)]
pub struct GithubAuthParams {
pub private_key: Vec<u8>,
pub installation_id: u64,
pub app_id: u64,
}