summaryrefslogtreecommitdiff
path: root/src/auth.rs
diff options
context:
space:
mode:
authorIan Wahbe <ian@wahbe.com>2022-11-17 07:32:14 -0800
committerGitHub <noreply@github.com>2022-11-17 16:32:14 +0100
commit837cae3b64c8eedb030a625ce5261d7b46ae71e8 (patch)
treea081afec01809459baf5fa63fa225015d84264f4 /src/auth.rs
parentf5410cd77cefb948a6d3579929f07747ff25ad4f (diff)
Add OAuth + device flow (#248)
* Add OAuth flow * Cleanup comments * Respond to comments * Make DeviceCodes unconstructable * Allow builder to accept OAuth
Diffstat (limited to 'src/auth.rs')
-rw-r--r--src/auth.rs178
1 files changed, 175 insertions, 3 deletions
diff --git a/src/auth.rs b/src/auth.rs
index 680de63..f147f9f 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -2,9 +2,10 @@
use crate::models::AppId;
use crate::Result;
+use either::Either;
use jsonwebtoken::{Algorithm, EncodingKey, Header};
-use secrecy::SecretString;
-use serde::Serialize;
+use secrecy::{ExposeSecret, SecretString};
+use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::SystemTime;
@@ -32,7 +33,7 @@ pub enum Auth {
/// No authentication
None,
// Basic HTTP authentication (username:password)
- Basic{
+ Basic {
/// Username
username: String,
/// Password
@@ -42,6 +43,8 @@ pub enum Auth {
PersonalToken(SecretString),
/// Authenticate as a Github App
App(AppAuth),
+ /// Authenticate as a Github OAuth App
+ OAuth(OAuth),
}
impl Default for Auth {
@@ -87,3 +90,172 @@ impl AppAuth {
create_jwt(self.app_id, &self.key).context(crate::error::JWTSnafu)
}
}
+
+/// The data necessary to authenticate as a github OAtuh app.
+#[derive(Clone, Deserialize)]
+#[serde(from = "OAuthWire")]
+pub struct OAuth {
+ pub access_token: SecretString,
+ pub token_type: String,
+ pub scope: Vec<String>,
+}
+
+/// The wire format of the OAuth struct.
+#[derive(Deserialize)]
+struct OAuthWire {
+ access_token: String,
+ token_type: String,
+ scope: String,
+}
+
+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(),
+ }
+ }
+}
+
+impl crate::Octocrab {
+ /// Authenticate with Github's device flow. This starts the process to obtain a new `OAuth`.
+ ///
+ /// See https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#device-flow for details.
+ ///
+ /// Note: To authenticate against public Github, the `Octocrab` that calls this method
+ /// *must* be constructed with `base_url: "https://github.com"` and extra header
+ /// "ACCEPT: application/json". For example:
+ /// ```no_run
+ /// # async fn run() -> octocrab::Result<()> {
+ /// # use reqwest::header::ACCEPT;
+ /// let crab = octocrab::Octocrab::builder()
+ /// .base_url("https://github.com")?
+ /// .add_header(ACCEPT, "application/json".to_string())
+ /// .build()?;
+ /// # Ok(())
+ /// # }
+ /// ```
+ 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)
+ }
+}
+
+/// The device codes as returned from step 1 of Github's device flow.
+///
+/// See https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#response-parameters
+#[derive(Deserialize, Clone)]
+#[non_exhaustive]
+pub struct DeviceCodes {
+ /// The device verification code is 40 characters and used to verify the device.
+ pub device_code: String,
+ /// The user verification code is displayed on the device so the user can enter the
+ /// code in a browser. This code is 8 characters with a hyphen in the middle.
+ pub user_code: String,
+ /// The verification URL where users need to enter the user_code: https://github.com/login/device.
+ pub verification_uri: String,
+ /// The number of seconds before the device_code and user_code expire. The default is
+ /// 900 seconds or 15 minutes.
+ pub expires_in: u64,
+ /// The minimum number of seconds that must pass before you can make a new access
+ /// token request (POST https://github.com/login/oauth/access_token) to complete the
+ /// device authorization. For example, if the interval is 5, then you cannot make a
+ /// new request until 5 seconds pass. If you make more than one request over 5
+ /// seconds, then you will hit the rate limit and receive a slow_down error.
+ pub interval: u64,
+}
+
+impl DeviceCodes {
+ /// Poll Github to see if authentication codes are available.
+ ///
+ /// See `https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#response-parameters` for details.
+ 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),
+ })
+ }
+}
+
+/// See https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#input-parameters
+#[derive(Serialize)]
+struct DeviceFlow<'a> {
+ client_id: &'a str,
+ scope: &'a str,
+}
+
+#[derive(Deserialize)]
+#[serde(untagged)]
+enum TokenResponse {
+ // We got the auth information.
+ Ok(OAuth),
+ // We got an error that allows us to continue polling.
+ Contine { error: Continue },
+}
+
+/// Control flow when polling the device flow authorization.
+#[derive(Deserialize, Debug, Clone, Copy)]
+#[serde(rename_all = "snake_case")]
+pub enum Continue {
+ /// When you receive the slow_down error, 5 extra seconds are added to the minimum
+ /// interval or timeframe required between your requests using POST
+ /// https://github.com/login/oauth/access_token. For example, if the starting interval
+ /// required at least 5 seconds between requests and you get a slow_down error response,
+ /// you must now wait a minimum of 10 seconds before making a new request for an OAuth
+ /// access token. The error response includes the new interval that you must use.
+ SlowDown,
+ /// This error occurs when the authorization request is pending and the user hasn't
+ /// entered the user code yet. The app is expected to keep polling the POST
+ /// https://github.com/login/oauth/access_token request without exceeding the
+ /// interval, which requires a minimum number of seconds between each request.
+ AuthorizationPending,
+}
+
+#[derive(Serialize)]
+struct PollForDevice<'a> {
+ /// Required. The client ID you received from GitHub for your OAuth App.
+ client_id: &'a str,
+ /// Required. The device verification code you received from the POST https://github.com/login/device/code request.
+ device_code: &'a str,
+ /// Required. The grant type must be urn:ietf:params:oauth:grant-type:device_code.
+ grant_type: &'static str,
+}