From 05950c332405f0778cfa45e599b8d43577213fb5 Mon Sep 17 00:00:00 2001 From: Mattia Pitossi Date: Mon, 23 Jun 2025 18:40:10 +0200 Subject: [PATCH 1/8] feat: add login oauth for web application flow --- src/auth.rs | 37 +++++++++++++++++ tests/auth_oauth_flow_test.rs | 78 +++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 tests/auth_oauth_flow_test.rs diff --git a/src/auth.rs b/src/auth.rs index ecb2b215..9fb21536 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -258,6 +258,31 @@ impl DeviceCodes { } } +/// Exchange a code for a user access token +/// +/// see: https://docs.github.com/en/developers/apps/identifying-and-authorizing-users-for-github-apps +pub async fn get_access_token( + crab: &crate::Octocrab, + client_id: &SecretString, + code: String, + client_secret: &SecretString, + redirect_uri: String, +) -> Result { + let data: OAuth = crab + .post( + "/login/oauth/access_token", + Some(&ExchangeCodeForTokenParams { + client_id: client_id.expose_secret(), + client_secret: client_secret.expose_secret(), + code: &code, + redirect_uri: &redirect_uri, + }), + ) + .await?; + + Ok(data) +} + /// See https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#input-parameters #[derive(Serialize)] struct DeviceFlow<'a> { @@ -301,3 +326,15 @@ struct PollForDevice<'a> { /// Required. The grant type must be urn:ietf:params:oauth:grant-type:device_code. grant_type: &'static str, } + +#[derive(Serialize)] +struct ExchangeCodeForTokenParams<'a> { + /// Required. The client ID for your GitHub App. + client_id: &'a str, + /// Required. The client secret for your GitHub App. + client_secret: &'a str, + /// Required. The code received from the POST request. + code: &'a str, + // Strongly recommended. The URL in your application where users will be sent after authorization. + redirect_uri: &'a str, +} diff --git a/tests/auth_oauth_flow_test.rs b/tests/auth_oauth_flow_test.rs new file mode 100644 index 00000000..3f040376 --- /dev/null +++ b/tests/auth_oauth_flow_test.rs @@ -0,0 +1,78 @@ +mod mock_error; + +use mock_error::setup_error_handler; +use octocrab::{auth, Octocrab}; +use secrecy::SecretString; +use serde_json::json; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +async fn setup_post_api(template: ResponseTemplate) -> MockServer { + let mock_server = MockServer::start().await; + + Mock::given(method("POST")) + .and(path(format!("/login/oauth/access_token"))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + + setup_error_handler( + &mock_server, + &format!("POST on /login/oauth/access_token was not received"), + ) + .await; + mock_server +} + +fn setup_octocrab(uri: &str) -> Octocrab { + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() +} + +const CLIENT_SECRET: &str = "some_secret"; +const CLIENT_ID: &str = "some_client_id"; +const CODE: &str = "a_code"; +const REDIRECT_URI: &str = "https://yourapp/auth/callback-example"; + +#[tokio::test] +async fn should_return_access_token() { + let expected_response = json!({ + "access_token":"gho_16C7e42F292c6912E7710c838347Ae178B4a", + "scope":"repo,gist", + "token_type":"bearer" + } + ); + let template = ResponseTemplate::new(201).set_body_json(expected_response); + let mock_server = setup_post_api(template).await; + let client = setup_octocrab(&mock_server.uri()); + + let result = auth::get_access_token( + &client, + &SecretString::from(CLIENT_ID), + CODE.to_owned(), + &SecretString::from(CLIENT_SECRET), + REDIRECT_URI.to_owned(), + ) + .await; + + assert!(result.is_ok()); +} + +#[tokio::test] +async fn should_fail_when_receving_a_server_error() { + let template = ResponseTemplate::new(500); + let mock_server = setup_post_api(template).await; + let client = setup_octocrab(&mock_server.uri()); + + let result = auth::get_access_token( + &client, + &SecretString::from(CLIENT_ID), + CODE.to_owned(), + &SecretString::from(CLIENT_SECRET), + REDIRECT_URI.to_owned(), + ) + .await; + + assert!(result.is_err()); +} From 374cdf402b2b19bb288e7cae6e15b4be4309013f Mon Sep 17 00:00:00 2001 From: Mattia Pitossi Date: Mon, 23 Jun 2025 18:41:52 +0200 Subject: [PATCH 2/8] chore: fmt code --- tests/auth_oauth_flow_test.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/auth_oauth_flow_test.rs b/tests/auth_oauth_flow_test.rs index 3f040376..f64e5fa2 100644 --- a/tests/auth_oauth_flow_test.rs +++ b/tests/auth_oauth_flow_test.rs @@ -38,11 +38,11 @@ const REDIRECT_URI: &str = "https://yourapp/auth/callback-example"; #[tokio::test] async fn should_return_access_token() { let expected_response = json!({ - "access_token":"gho_16C7e42F292c6912E7710c838347Ae178B4a", - "scope":"repo,gist", - "token_type":"bearer" - } - ); + "access_token":"gho_16C7e42F292c6912E7710c838347Ae178B4a", + "scope":"repo,gist", + "token_type":"bearer" + } + ); let template = ResponseTemplate::new(201).set_body_json(expected_response); let mock_server = setup_post_api(template).await; let client = setup_octocrab(&mock_server.uri()); From c56462c2c3cc6a1a5a386a8c25be0dc5a36f3d9f Mon Sep 17 00:00:00 2001 From: Mattia Pitossi Date: Sat, 2 Aug 2025 12:57:24 +0200 Subject: [PATCH 3/8] feat: introduce builder for exchange web app flow code --- src/api.rs | 1 + src/api/auth.rs | 20 +++++++++++++ src/api/auth/exchange_web_flow_code.rs | 41 ++++++++++++++++++++++++++ src/auth.rs | 3 +- 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/api/auth.rs create mode 100644 src/api/auth/exchange_web_flow_code.rs diff --git a/src/api.rs b/src/api.rs index 9dc161c5..19c4a751 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,6 +1,7 @@ pub mod actions; pub mod activity; pub mod apps; +pub mod auth; pub mod checks; pub mod code_scannings; pub mod commits; diff --git a/src/api/auth.rs b/src/api/auth.rs new file mode 100644 index 00000000..8bedc292 --- /dev/null +++ b/src/api/auth.rs @@ -0,0 +1,20 @@ +use exchange_web_flow_code::ExchangeWebFlowCodeBuilder; + +use crate::Octocrab; + +pub mod exchange_web_flow_code; + +pub struct ExchangeWebFlowCodeHandler<'octo> { + crab: &'octo Octocrab, +} + +impl<'octo> ExchangeWebFlowCodeHandler<'octo> { + pub(crate) fn new(crab: &'octo Octocrab) -> Self { + Self { crab } + } + + pub fn exchange_token(&self) -> ExchangeWebFlowCodeBuilder<'_, '_, '_, '_> { + //TODO: add params + ExchangeWebFlowCodeBuilder::new(self.crab) + } +} diff --git a/src/api/auth/exchange_web_flow_code.rs b/src/api/auth/exchange_web_flow_code.rs new file mode 100644 index 00000000..28b555b4 --- /dev/null +++ b/src/api/auth/exchange_web_flow_code.rs @@ -0,0 +1,41 @@ +use secrecy::{ExposeSecret, SecretString}; + +use crate::Octocrab; + +#[derive(serde::Serialize)] +pub struct ExchangeWebFlowCodeBuilder<'octo, 'client_id, 'code, 'client_secret> { + #[serde(skip)] + crab: &'octo Octocrab, + client_id: &'client_id str, + #[serde(skip_serializing_if = "Option::is_none")] + code: Option<&'code str>, + client_secret: &'client_secret str, + #[serde(skip_serializing_if = "Option::is_none")] + redirect_uri: Option, +} + +impl<'octo, 'client_id, 'code, 'client_secret> + ExchangeWebFlowCodeBuilder<'octo, 'client_id, 'code, 'client_secret> +{ + pub(crate) fn new( + crab: &'octo Octocrab, + client_id: &'client_id SecretString, + code: Option<&'code str>, + client_secret: &'client_secret SecretString, + redirect_uri: Option, + ) -> Self { + Self { + crab, + client_id: client_id.expose_secret(), + code, + client_secret: client_secret.expose_secret(), + redirect_uri, + } + } + + /// Sends the actual request. + pub async fn send(self) -> crate::Result { + let route = "/login/oauth/access_token"; + self.crab.post(route, Some(&self)).await + } +} diff --git a/src/auth.rs b/src/auth.rs index 9fb21536..3e64f6c1 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,7 +1,8 @@ //! Authentication related types and functions. -use crate::models::AppId; +use crate::repos::RepoHandler; use crate::Result; +use crate::{models::AppId, Octocrab}; use either::Either; use jsonwebtoken::{Algorithm, EncodingKey, Header}; use secrecy::{ExposeSecret, SecretString}; From 7acda1dcc289d0fa8bebbbb400abb324924681be Mon Sep 17 00:00:00 2001 From: Mattia Pitossi Date: Sun, 3 Aug 2025 09:48:32 +0200 Subject: [PATCH 4/8] feat: introduce builder for exchange web app flow code --- src/api.rs | 1 - src/api/auth.rs | 20 ------ src/api/auth/exchange_web_flow_code.rs | 41 ------------- src/auth.rs | 84 +++++++++++++++----------- tests/auth_oauth_flow_test.rs | 78 ------------------------ 5 files changed, 49 insertions(+), 175 deletions(-) delete mode 100644 src/api/auth.rs delete mode 100644 src/api/auth/exchange_web_flow_code.rs delete mode 100644 tests/auth_oauth_flow_test.rs diff --git a/src/api.rs b/src/api.rs index 19c4a751..9dc161c5 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,6 @@ pub mod actions; pub mod activity; pub mod apps; -pub mod auth; pub mod checks; pub mod code_scannings; pub mod commits; diff --git a/src/api/auth.rs b/src/api/auth.rs deleted file mode 100644 index 8bedc292..00000000 --- a/src/api/auth.rs +++ /dev/null @@ -1,20 +0,0 @@ -use exchange_web_flow_code::ExchangeWebFlowCodeBuilder; - -use crate::Octocrab; - -pub mod exchange_web_flow_code; - -pub struct ExchangeWebFlowCodeHandler<'octo> { - crab: &'octo Octocrab, -} - -impl<'octo> ExchangeWebFlowCodeHandler<'octo> { - pub(crate) fn new(crab: &'octo Octocrab) -> Self { - Self { crab } - } - - pub fn exchange_token(&self) -> ExchangeWebFlowCodeBuilder<'_, '_, '_, '_> { - //TODO: add params - ExchangeWebFlowCodeBuilder::new(self.crab) - } -} diff --git a/src/api/auth/exchange_web_flow_code.rs b/src/api/auth/exchange_web_flow_code.rs deleted file mode 100644 index 28b555b4..00000000 --- a/src/api/auth/exchange_web_flow_code.rs +++ /dev/null @@ -1,41 +0,0 @@ -use secrecy::{ExposeSecret, SecretString}; - -use crate::Octocrab; - -#[derive(serde::Serialize)] -pub struct ExchangeWebFlowCodeBuilder<'octo, 'client_id, 'code, 'client_secret> { - #[serde(skip)] - crab: &'octo Octocrab, - client_id: &'client_id str, - #[serde(skip_serializing_if = "Option::is_none")] - code: Option<&'code str>, - client_secret: &'client_secret str, - #[serde(skip_serializing_if = "Option::is_none")] - redirect_uri: Option, -} - -impl<'octo, 'client_id, 'code, 'client_secret> - ExchangeWebFlowCodeBuilder<'octo, 'client_id, 'code, 'client_secret> -{ - pub(crate) fn new( - crab: &'octo Octocrab, - client_id: &'client_id SecretString, - code: Option<&'code str>, - client_secret: &'client_secret SecretString, - redirect_uri: Option, - ) -> Self { - Self { - crab, - client_id: client_id.expose_secret(), - code, - client_secret: client_secret.expose_secret(), - redirect_uri, - } - } - - /// Sends the actual request. - pub async fn send(self) -> crate::Result { - let route = "/login/oauth/access_token"; - self.crab.post(route, Some(&self)).await - } -} diff --git a/src/auth.rs b/src/auth.rs index 3e64f6c1..871e991a 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,6 +1,5 @@ //! Authentication related types and functions. -use crate::repos::RepoHandler; use crate::Result; use crate::{models::AppId, Octocrab}; use either::Either; @@ -259,29 +258,56 @@ impl DeviceCodes { } } -/// Exchange a code for a user access token -/// -/// see: https://docs.github.com/en/developers/apps/identifying-and-authorizing-users-for-github-apps -pub async fn get_access_token( - crab: &crate::Octocrab, - client_id: &SecretString, - code: String, - client_secret: &SecretString, - redirect_uri: String, -) -> Result { - let data: OAuth = crab - .post( - "/login/oauth/access_token", - Some(&ExchangeCodeForTokenParams { - client_id: client_id.expose_secret(), - client_secret: client_secret.expose_secret(), - code: &code, - redirect_uri: &redirect_uri, - }), - ) - .await?; +#[derive(serde::Serialize)] +pub struct ExchangeWebFlowCodeBuilder<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri> { + #[serde(skip)] + crab: &'octo Octocrab, + client_id: &'client_id str, + #[serde(skip_serializing_if = "Option::is_none")] + code: Option<&'code str>, + client_secret: &'client_secret str, + #[serde(skip_serializing_if = "Option::is_none")] + redirect_uri: Option<&'redirect_uri str>, +} + +impl<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri> + ExchangeWebFlowCodeBuilder<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri> +{ + pub fn new( + crab: &'octo Octocrab, + client_id: &'client_id SecretString, + client_secret: &'client_secret SecretString, + ) -> Self { + Self { + crab, + client_id: client_id.expose_secret(), + code: None, + client_secret: client_secret.expose_secret(), + redirect_uri: None, + } + } + + /// Set the `code` for exchange web flow code request to be created. + pub fn code(mut self, code: &'code str) -> Self { + self.code = Some(code); + self + } + + /// Set the `redirect_uri` for exchange web flow code request to be created. + pub fn redirect_uri(mut self, redirect_uri: &'redirect_uri str) -> Self { + self.redirect_uri = Some(redirect_uri); + self + } - Ok(data) + /// Sends the actual request. + /// Exchange a code for a user access token + /// + /// see: https://docs.github.com/en/developers/apps/identifying-and-authorizing-users-for-github-apps + /// + pub async fn send(self) -> crate::Result { + let route = "/login/oauth/access_token"; + self.crab.post(route, Some(&self)).await + } } /// See https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#input-parameters @@ -327,15 +353,3 @@ struct PollForDevice<'a> { /// Required. The grant type must be urn:ietf:params:oauth:grant-type:device_code. grant_type: &'static str, } - -#[derive(Serialize)] -struct ExchangeCodeForTokenParams<'a> { - /// Required. The client ID for your GitHub App. - client_id: &'a str, - /// Required. The client secret for your GitHub App. - client_secret: &'a str, - /// Required. The code received from the POST request. - code: &'a str, - // Strongly recommended. The URL in your application where users will be sent after authorization. - redirect_uri: &'a str, -} diff --git a/tests/auth_oauth_flow_test.rs b/tests/auth_oauth_flow_test.rs deleted file mode 100644 index f64e5fa2..00000000 --- a/tests/auth_oauth_flow_test.rs +++ /dev/null @@ -1,78 +0,0 @@ -mod mock_error; - -use mock_error::setup_error_handler; -use octocrab::{auth, Octocrab}; -use secrecy::SecretString; -use serde_json::json; -use wiremock::{ - matchers::{method, path}, - Mock, MockServer, ResponseTemplate, -}; - -async fn setup_post_api(template: ResponseTemplate) -> MockServer { - let mock_server = MockServer::start().await; - - Mock::given(method("POST")) - .and(path(format!("/login/oauth/access_token"))) - .respond_with(template.clone()) - .mount(&mock_server) - .await; - - setup_error_handler( - &mock_server, - &format!("POST on /login/oauth/access_token was not received"), - ) - .await; - mock_server -} - -fn setup_octocrab(uri: &str) -> Octocrab { - Octocrab::builder().base_uri(uri).unwrap().build().unwrap() -} - -const CLIENT_SECRET: &str = "some_secret"; -const CLIENT_ID: &str = "some_client_id"; -const CODE: &str = "a_code"; -const REDIRECT_URI: &str = "https://yourapp/auth/callback-example"; - -#[tokio::test] -async fn should_return_access_token() { - let expected_response = json!({ - "access_token":"gho_16C7e42F292c6912E7710c838347Ae178B4a", - "scope":"repo,gist", - "token_type":"bearer" - } - ); - let template = ResponseTemplate::new(201).set_body_json(expected_response); - let mock_server = setup_post_api(template).await; - let client = setup_octocrab(&mock_server.uri()); - - let result = auth::get_access_token( - &client, - &SecretString::from(CLIENT_ID), - CODE.to_owned(), - &SecretString::from(CLIENT_SECRET), - REDIRECT_URI.to_owned(), - ) - .await; - - assert!(result.is_ok()); -} - -#[tokio::test] -async fn should_fail_when_receving_a_server_error() { - let template = ResponseTemplate::new(500); - let mock_server = setup_post_api(template).await; - let client = setup_octocrab(&mock_server.uri()); - - let result = auth::get_access_token( - &client, - &SecretString::from(CLIENT_ID), - CODE.to_owned(), - &SecretString::from(CLIENT_SECRET), - REDIRECT_URI.to_owned(), - ) - .await; - - assert!(result.is_err()); -} From b14d9bb0aa4f89af59afe3995ad544078759323b Mon Sep 17 00:00:00 2001 From: Mattia Pitossi Date: Sun, 3 Aug 2025 09:49:15 +0200 Subject: [PATCH 5/8] feat(tests) add tests for exchange web flow code --- tests/auth_exchange_web_flow_code_test.rs | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/auth_exchange_web_flow_code_test.rs diff --git a/tests/auth_exchange_web_flow_code_test.rs b/tests/auth_exchange_web_flow_code_test.rs new file mode 100644 index 00000000..ca0316db --- /dev/null +++ b/tests/auth_exchange_web_flow_code_test.rs @@ -0,0 +1,81 @@ +mod mock_error; + +use mock_error::setup_error_handler; +use octocrab::{ + auth::{self, ExchangeWebFlowCodeBuilder}, + Octocrab, +}; +use secrecy::SecretString; +use serde_json::json; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +async fn setup_post_api(template: ResponseTemplate) -> MockServer { + let mock_server = MockServer::start().await; + + Mock::given(method("POST")) + .and(path(format!("/login/oauth/access_token"))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + + setup_error_handler( + &mock_server, + &format!("POST on /login/oauth/access_token was not received"), + ) + .await; + mock_server +} + +fn setup_octocrab(uri: &str) -> Octocrab { + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() +} + +const CLIENT_SECRET: &str = "some_secret"; +const CLIENT_ID: &str = "some_client_id"; +const CODE: &str = "a_code"; +const REDIRECT_URI: &str = "https://yourapp/auth/callback-example"; + +#[tokio::test] +async fn should_return_oauth_response() { + let expected_response = json!({ + "access_token":"gho_16C7e42F292c6912E7710c838347Ae178B4a", + "scope":"repo,gist", + "token_type":"bearer" + } + ); + let template = ResponseTemplate::new(201).set_body_json(expected_response); + let mock_server = setup_post_api(template).await; + let client = setup_octocrab(&mock_server.uri()); + + let result = auth::ExchangeWebFlowCodeBuilder::new( + &client, + &SecretString::from(CLIENT_ID), + &SecretString::from(CLIENT_SECRET), + ) + .code(CODE) + .redirect_uri(REDIRECT_URI.to_owned()) + .send() + .await; + + assert!(result.is_ok()); +} + +#[tokio::test] +async fn should_fail_when_receving_a_server_error() { + let template = ResponseTemplate::new(500); + let mock_server = setup_post_api(template).await; + let client = setup_octocrab(&mock_server.uri()); + + let result = auth::ExchangeWebFlowCodeBuilder::new( + &client, + &SecretString::from(CLIENT_ID), + &SecretString::from(CLIENT_SECRET), + ) + .send() + .await; + + assert!(result.is_err()); +} From c0b5daddce8dc10233cd814df726ecd406242c1e Mon Sep 17 00:00:00 2001 From: Mattia Pitossi Date: Mon, 4 Aug 2025 08:43:34 +0200 Subject: [PATCH 6/8] feat(docs): add comment linking the web application flow --- src/auth.rs | 1 + tests/auth_exchange_web_flow_code_test.rs | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 871e991a..a7aa8786 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -258,6 +258,7 @@ impl DeviceCodes { } } +/// See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-web-application-flow-to-generate-a-user-access-token #[derive(serde::Serialize)] pub struct ExchangeWebFlowCodeBuilder<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri> { #[serde(skip)] diff --git a/tests/auth_exchange_web_flow_code_test.rs b/tests/auth_exchange_web_flow_code_test.rs index ca0316db..d09ed511 100644 --- a/tests/auth_exchange_web_flow_code_test.rs +++ b/tests/auth_exchange_web_flow_code_test.rs @@ -1,10 +1,7 @@ mod mock_error; use mock_error::setup_error_handler; -use octocrab::{ - auth::{self, ExchangeWebFlowCodeBuilder}, - Octocrab, -}; +use octocrab::{auth, Octocrab}; use secrecy::SecretString; use serde_json::json; use wiremock::{ @@ -56,7 +53,7 @@ async fn should_return_oauth_response() { &SecretString::from(CLIENT_SECRET), ) .code(CODE) - .redirect_uri(REDIRECT_URI.to_owned()) + .redirect_uri(REDIRECT_URI) .send() .await; From ca9de412dddbbccc5c9278cc2b05abb78898545c Mon Sep 17 00:00:00 2001 From: Mattia Pitossi Date: Mon, 4 Aug 2025 09:02:28 +0200 Subject: [PATCH 7/8] feat: add code_verifier and repository_id for web application flow --- src/auth.rs | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index a7aa8786..00b3722f 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -260,19 +260,45 @@ impl DeviceCodes { /// See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-web-application-flow-to-generate-a-user-access-token #[derive(serde::Serialize)] -pub struct ExchangeWebFlowCodeBuilder<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri> { +pub struct ExchangeWebFlowCodeBuilder< + 'octo, + 'client_id, + 'code, + 'client_secret, + 'redirect_uri, + 'code_verifier, + 'repository_id, +> { #[serde(skip)] crab: &'octo Octocrab, + /// The client ID for your GitHub App. client_id: &'client_id str, #[serde(skip_serializing_if = "Option::is_none")] + /// The code you received in the previous step. code: Option<&'code str>, + /// The client secret for your GitHub App. client_secret: &'client_secret str, #[serde(skip_serializing_if = "Option::is_none")] + /// The URL in your application where users will be sent after authorization. redirect_uri: Option<&'redirect_uri str>, + #[serde(skip_serializing_if = "Option::is_none")] + /// For the PKCE challenge. + code_verifier: Option<&'code_verifier str>, + #[serde(skip_serializing_if = "Option::is_none")] + /// The ID of a single repository that the user access token can access. + repository_id: Option<&'repository_id str>, } -impl<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri> - ExchangeWebFlowCodeBuilder<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri> +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, @@ -285,6 +311,8 @@ impl<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri> code: None, client_secret: client_secret.expose_secret(), redirect_uri: None, + code_verifier: None, + repository_id: None, } } From f3223da4b99d10c83833cb3961d1fa945db823ed Mon Sep 17 00:00:00 2001 From: Mattia Date: Sun, 24 Aug 2025 15:14:02 +0200 Subject: [PATCH 8/8] fix: make code mandatory --- src/auth.rs | 30 ++++++++++++++--------- tests/auth_exchange_web_flow_code_test.rs | 3 ++- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 00b3722f..b030c473 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -273,19 +273,18 @@ pub struct ExchangeWebFlowCodeBuilder< crab: &'octo Octocrab, /// The client ID for your GitHub App. client_id: &'client_id str, - #[serde(skip_serializing_if = "Option::is_none")] /// The code you received in the previous step. - code: Option<&'code str>, + code: &'code str, /// The client secret for your GitHub App. client_secret: &'client_secret str, - #[serde(skip_serializing_if = "Option::is_none")] /// The URL in your application where users will be sent after authorization. - redirect_uri: Option<&'redirect_uri str>, #[serde(skip_serializing_if = "Option::is_none")] + redirect_uri: Option<&'redirect_uri str>, /// For the PKCE challenge. - code_verifier: Option<&'code_verifier str>, #[serde(skip_serializing_if = "Option::is_none")] + code_verifier: Option<&'code_verifier str>, /// The ID of a single repository that the user access token can access. + #[serde(skip_serializing_if = "Option::is_none")] repository_id: Option<&'repository_id str>, } @@ -304,11 +303,12 @@ impl<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri, 'code_verifier, 'r 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: None, + code, client_secret: client_secret.expose_secret(), redirect_uri: None, code_verifier: None, @@ -316,18 +316,24 @@ impl<'octo, 'client_id, 'code, 'client_secret, 'redirect_uri, 'code_verifier, 'r } } - /// Set the `code` for exchange web flow code request to be created. - pub fn code(mut self, code: &'code str) -> Self { - self.code = Some(code); - self - } - /// Set the `redirect_uri` for exchange web flow code request to be created. pub fn redirect_uri(mut self, redirect_uri: &'redirect_uri str) -> Self { self.redirect_uri = Some(redirect_uri); self } + /// Set the `code_verifier` for exchange web flow code request to be created. + pub fn code_verifier(mut self, code_verifier: &'code_verifier str) -> Self { + self.code_verifier = Some(code_verifier); + self + } + + /// Set the `repository_id` for exchange web flow code request to be created. + pub fn repository_id(mut self, repository_id: &'repository_id str) -> Self { + self.repository_id = Some(repository_id); + self + } + /// Sends the actual request. /// Exchange a code for a user access token /// diff --git a/tests/auth_exchange_web_flow_code_test.rs b/tests/auth_exchange_web_flow_code_test.rs index d09ed511..89d714a4 100644 --- a/tests/auth_exchange_web_flow_code_test.rs +++ b/tests/auth_exchange_web_flow_code_test.rs @@ -51,8 +51,8 @@ async fn should_return_oauth_response() { &client, &SecretString::from(CLIENT_ID), &SecretString::from(CLIENT_SECRET), + CODE, ) - .code(CODE) .redirect_uri(REDIRECT_URI) .send() .await; @@ -70,6 +70,7 @@ async fn should_fail_when_receving_a_server_error() { &client, &SecretString::from(CLIENT_ID), &SecretString::from(CLIENT_SECRET), + CODE, ) .send() .await;