use crate::Result;
use crate::{models::AppId, Octocrab};
use jsonwebtoken::{Algorithm, EncodingKey, Header};
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(feature = "tokio")]
use web_time::Duration;
use web_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<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
}
#[cfg(feature = "tokio")]
pub async fn poll_until_available(
&self,
crab: &crate::Octocrab,
client_id: &SecretString,
) -> Result<OAuth> {
let mut interval = Duration::from_secs(self.interval);
let mut clock = tokio::time::interval(interval);
loop {
clock.tick().await;
match self.poll_once(crab, client_id).await? {
TokenResponse::Ok(auth) => return Ok(auth),
TokenResponse::Continue { error } => match error {
Continue::SlowDown => {
interval += Duration::from_secs(5);
clock = tokio::time::interval(interval);
clock.tick().await;
}
Continue::AuthorizationPending => {
}
},
}
}
}
}
#[derive(serde::Serialize)]
pub struct ExchangeWebFlowCodeBuilder<
'octo,
'client_id,
'code,
'client_secret,
'redirect_uri,
'code_verifier,
'repository_id,
> {
#[serde(skip)]
crab: &'octo Octocrab,
client_id: &'client_id str,
code: &'code str,
client_secret: &'client_secret str,
#[serde(skip_serializing_if = "Option::is_none")]
redirect_uri: Option<&'redirect_uri str>,
#[serde(skip_serializing_if = "Option::is_none")]
code_verifier: Option<&'code_verifier str>,
#[serde(skip_serializing_if = "Option::is_none")]
repository_id: Option<&'repository_id str>,
}
impl<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri, 'code_verifier, 'repository_id>
ExchangeWebFlowCodeBuilder<
'octo,
'client_id,
'code,
'client_secret,
'redirect_uri,
'code_verifier,
'repository_id,
>
{
pub fn new(
crab: &'octo Octocrab,
client_id: &'client_id SecretString,
client_secret: &'client_secret SecretString,
code: &'code str,
) -> Self {
Self {
crab,
client_id: client_id.expose_secret(),
code,
client_secret: client_secret.expose_secret(),
redirect_uri: None,
code_verifier: None,
repository_id: None,
}
}
pub fn redirect_uri(mut self, redirect_uri: &'redirect_uri str) -> Self {
self.redirect_uri = Some(redirect_uri);
self
}
pub fn code_verifier(mut self, code_verifier: &'code_verifier str) -> Self {
self.code_verifier = Some(code_verifier);
self
}
pub fn repository_id(mut self, repository_id: &'repository_id str) -> Self {
self.repository_id = Some(repository_id);
self
}
pub async fn send(self) -> crate::Result<OAuth> {
let route = "/login/oauth/access_token";
self.crab.post(route, Some(&self)).await
}
}
#[derive(Serialize)]
struct DeviceFlow<'a> {
client_id: &'a str,
scope: &'a str,
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum TokenResponse {
Ok(OAuth),
Continue { 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,
}