diff options
author | Ian Wahbe <ian@wahbe.com> | 2022-11-17 07:32:14 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-17 16:32:14 +0100 |
commit | 837cae3b64c8eedb030a625ce5261d7b46ae71e8 (patch) | |
tree | a081afec01809459baf5fa63fa225015d84264f4 /src/auth.rs | |
parent | f5410cd77cefb948a6d3579929f07747ff25ad4f (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.rs | 178 |
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, +} |