use crate::models::AppId;
use crate::Result;
use either::Either;
use jsonwebtoken::{Algorithm, EncodingKey, Header};
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::SystemTime;
use snafu::*;
#[derive(Clone)]
pub struct AppAuth {
pub app_id: AppId,
pub key: EncodingKey,
}
impl fmt::Debug for AppAuth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AppAuth")
.field("app_id", &self.app_id)
.finish_non_exhaustive()
}
}
pub enum Auth {
None,
Basic {
username: String,
password: String,
},
PersonalToken(SecretString),
App(AppAuth),
OAuth(OAuth),
UserAccessToken(SecretString),
}
impl Default for Auth {
fn default() -> Self {
Self::None
}
}
pub fn create_jwt(
github_app_id: AppId,
key: &EncodingKey,
) -> Result<String, jsonwebtoken::errors::Error> {
#[derive(Serialize)]
struct Claims {
iss: AppId,
iat: usize,
exp: usize,
}
let now = SystemTime::UNIX_EPOCH.elapsed().unwrap().as_secs() as usize;
let claims = Claims {
iss: github_app_id,
iat: now - 60,
exp: now + (9 * 60),
};
let header = Header::new(Algorithm::RS256);
jsonwebtoken::encode(&header, &claims, key)
}
impl AppAuth {
pub fn generate_bearer_token(&self) -> Result<String> {
create_jwt(self.app_id, &self.key).context(crate::error::JWTSnafu)
}
}
#[derive(Clone, Deserialize)]
#[serde(from = "OAuthWire")]
pub struct OAuth {
pub access_token: SecretString,
pub token_type: String,
pub scope: Vec<String>,
pub expires_in: Option<usize>,
pub refresh_token: Option<SecretString>,
pub refresh_token_expires_in: Option<usize>,
}
#[derive(Deserialize)]
struct OAuthWire {
access_token: String,
token_type: String,
scope: String,
expires_in: Option<usize>,
refresh_token: Option<String>,
refresh_token_expires_in: Option<usize>,
}
impl From<OAuthWire> for OAuth {
fn from(value: OAuthWire) -> Self {
OAuth {
access_token: SecretString::from(value.access_token),
token_type: value.token_type,
scope: value.scope.split(',').map(ToString::to_string).collect(),
expires_in: value.expires_in,
refresh_token: value.refresh_token.map(SecretString::from),
refresh_token_expires_in: value.refresh_token_expires_in,
}
}
}
impl crate::Octocrab {
pub async fn authenticate_as_device<I, S>(
&self,
client_id: &SecretString,
scope: I,
) -> Result<DeviceCodes>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let scope = {
let mut scopes = scope.into_iter();
let first = scopes
.next()
.map(|s| s.as_ref().to_string())
.unwrap_or_default();
scopes.fold(first, |i: String, n| i + "," + n.as_ref())
};
let codes: DeviceCodes = self
.post(
"/login/device/code",
Some(&DeviceFlow {
client_id: client_id.expose_secret(),
scope: &scope,
}),
)
.await?;
Ok(codes)
}
}
#[derive(Deserialize, Clone)]
#[non_exhaustive]
pub struct DeviceCodes {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub expires_in: u64,
pub interval: u64,
}
impl DeviceCodes {
pub async fn poll_once(
&self,
crab: &crate::Octocrab,
client_id: &SecretString,
) -> Result<Either<OAuth, Continue>> {
let poll: TokenResponse = crab
.post(
"/login/oauth/access_token",
Some(&PollForDevice {
client_id: client_id.expose_secret(),
device_code: &self.device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
)
.await?;
Ok(match poll {
TokenResponse::Ok(k) => Either::Left(k),
TokenResponse::Contine { error } => Either::Right(error),
})
}
}
#[derive(Serialize)]
struct DeviceFlow<'a> {
client_id: &'a str,
scope: &'a str,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum TokenResponse {
Ok(OAuth),
Contine { error: Continue },
}
#[derive(Deserialize, Debug, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Continue {
SlowDown,
AuthorizationPending,
}
#[derive(Serialize)]
struct PollForDevice<'a> {
client_id: &'a str,
device_code: &'a str,
grant_type: &'static str,
}