Skip to content
89 changes: 88 additions & 1 deletion src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Authentication related types and functions.

use crate::models::AppId;
use crate::Result;
use crate::{models::AppId, Octocrab};
use either::Either;
use jsonwebtoken::{Algorithm, EncodingKey, Header};
use secrecy::{ExposeSecret, SecretString};
Expand Down Expand Up @@ -258,6 +258,93 @@ 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)]
Copy link
Contributor

@kyle-leonhard kyle-leonhard Aug 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the second one, thanks!

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,
/// The code you received in the previous step.
code: &'code str,
/// The client secret for your GitHub App.
client_secret: &'client_secret str,
/// The URL in your application where users will be sent after authorization.
#[serde(skip_serializing_if = "Option::is_none")]
redirect_uri: Option<&'redirect_uri str>,
/// For the PKCE challenge.
#[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>,
}

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,
}
}

/// 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
///
/// see: https://docs.github.com/en/developers/apps/identifying-and-authorizing-users-for-github-apps
///
pub async fn send(self) -> crate::Result<OAuth> {
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
#[derive(Serialize)]
struct DeviceFlow<'a> {
Expand Down
79 changes: 79 additions & 0 deletions tests/auth_exchange_web_flow_code_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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_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,
)
.redirect_uri(REDIRECT_URI)
.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),
CODE,
)
.send()
.await;

assert!(result.is_err());
}
Loading