Skip to content

Commit

Permalink
Implement Device Code for Facebook
Browse files Browse the repository at this point in the history
  • Loading branch information
mochi-neko committed Feb 1, 2024
1 parent 9667d98 commit c3163af
Show file tree
Hide file tree
Showing 16 changed files with 516 additions and 56 deletions.
12 changes: 1 addition & 11 deletions examples/sign_in_with_facebook_oauth_credential_on_auth_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ use tokio::sync::{mpsc, Mutex};
use fars::oauth::AuthorizationCode;
use fars::oauth::AuthorizationCodeSession;
use fars::oauth::ClientId;
use fars::oauth::ClientSecret;
use fars::oauth::CsrfState;
use fars::oauth::FacebookAuthorizationCodeClient;
use fars::oauth::OAuthScope;
Expand Down Expand Up @@ -50,16 +49,7 @@ async fn handle_redirect(
) -> String {
// Check query parameters.
if let Some(error) = params.error {
eprintln!(
"Error: {}, {:}, {:}",
error,
params
.error_reason
.unwrap_or_default(),
params
.error_description
.unwrap_or_default(),
);
eprintln!("Error: {}", error,);
return "Error".to_string();
}

Expand Down
75 changes: 75 additions & 0 deletions examples/sign_in_with_facebook_oauth_credential_on_device_code.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! An example to sign in with Facebook OAuth credential by session-based interface
//! on the Device Code grant type of the OAuth 2.0.
//!
//! ```shell
//! $ cargo run --example sign_in_with_facebook_oauth_credential_on_device_code --features oauth
//! ```

#![cfg(feature = "oauth")]

use qrcode::render::unicode;
use std::collections::HashSet;

use fars::oauth::FacebookDeviceCodeClient;
use fars::oauth::OAuthScope;
use fars::ApiKey;
use fars::Config;
use fars::OAuthRequestUri;
use fars::ProviderId;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Get Client ID from the environment variables.
let app_id = std::env::var("FACEBOOK_CLIENT_ID")?;
let client_token = std::env::var("FACEBOOK_CLIENT_TOKEN")?;

// Create an OAuth client.
let oauth_client = FacebookDeviceCodeClient::new(app_id, client_token)?;

// Request authorization.
let session = oauth_client
.request_authorization(HashSet::from([
OAuthScope::open_id(),
OAuthScope::open_id_email(),
OAuthScope::open_id_profile(),
]))
.await?;

let verification_uri = session.verification_uri();
let user_code = session.user_code();

// Encode verification URI to QR code.
let qr_code = qrcode::QrCode::new(verification_uri)?;

// Render QR code as string on the terminal.
let qr_code_string = qr_code
.render::<unicode::Dense1x2>()
.dark_color(unicode::Dense1x2::Dark)
.light_color(unicode::Dense1x2::Light)
.build();

// Display the verification URI and user code to the terminal.
println!("Verification URI:\n{}", qr_code_string);
println!("User code: {}", user_code);

// Poll to token endpoint to exchange a device code into an access token.
let token = session
.poll_exchange_token(tokio::time::sleep, None)
.await?;

// Sign in with Facebook OAuth credential.
let config = Config::new(ApiKey::from_env()?);
let session = config
.sign_in_with_oauth_credential(
OAuthRequestUri::new("http://localhost:8080"),
token.create_idp_post_body(ProviderId::Facebook)?,
)
.await?;

println!(
"Succeeded to sign in with Facebook OAuth credential: {:?}",
session
);

Ok(())
}
20 changes: 11 additions & 9 deletions src/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,23 @@
//! ## Supported identity providers and grant types
//!
//! - [Google](https://developers.google.com/identity/protocols/oauth2)
//! - [x] Authorization Code grant type with client secret and PKCE for Web-Server apps.
//! - [x] Device grant type for limited-input devices.
//! - [x] Authorization Code grant type with client secret and PKCE for public clients (Web-Server apps).
//! - [x] Device Code grant type for browserless or input-constrained devices.
//! - [Facebook](https://developers.facebook.com/docs/facebook-login/guides/advanced/oidc-token)
//! - [x] Authorization Code grant type with PKCE for Web-Server, Web-Client, Mobile and Desktop apps.
//! - [x] Authorization Code grant type with PKCE for confidential clients (Web-Server apps) and public clients (Web-Client, Mobile and Desktop apps).
//! - [x] Device Code grant type for browserless or input-constrained devices.
//! - [GitHub](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps)
//! - [x] Authorization Code grant type with client secret for Web-Server apps.
//! - [ ] Device grant type for limited-input devices.
//! - [x] Authorization Code grant type with client secret for confidential clients (Web-Server apps).
//! - [ ] Device Code grant type for browserless or input-constrained devices.
//! - [Twitter (X)](https://developer.twitter.com/en/docs/authentication/oauth-2-0)
//! - [ ] Authorization Code grant type with PKCE for Web-Server, Web-Client, Mobile, and Desktop apps.
//! - [ ] Authorization Code grant type with PKCE for confidential clients (Web-Server apps) and public clients (Web-Client, Mobile, and Desktop apps).
//! - Implemented but may not be supported by the Firebase Auth.
//! - [Microsoft](https://learn.microsoft.com/en-us/entra/identity-platform/v2-app-types)
//! - [ ] Authorization Code grant type with PKCE for Web-Server, Web-Client, Mobile, and Desktop apps.
//! - [ ] Authorization Code grant type with PKCE for confidential clients (Web-Server apps) and public clients (Web-Client, Mobile, and Desktop apps).
//! - Implemented but may not be supported by the Firebase Auth.
//! - [ ] Device grant type for limited-input devices.
//! - [ ] Yahoo
//! - [ ] Device Code grant type for browserless or input-constrained devices.
//! - [ ] Apple
//! - [ ] Yahoo
//! - [ ] Google Play Games
//! - [ ] Apple Game Center
//!
Expand Down Expand Up @@ -61,6 +62,7 @@ pub use device_code_client::DeviceCodeClient;
pub use device_code_session::DeviceCodeSession;
pub use error::OAuthError;
pub use idp::facebook_auth_code::FacebookAuthorizationCodeClient;
pub use idp::facebook_device_code::FacebookDeviceCodeClient;
pub use idp::github_auth_code::GitHubAuthorizationCodeClient;
pub use idp::google_auth_code::GoogleAuthorizationCodeClient;
pub use idp::google_device_code::GoogleDeviceCodeClient;
Expand Down
9 changes: 6 additions & 3 deletions src/oauth/auth_code_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ use crate::oauth::TokenEndpoint;
/// This is only available when the feature "oauth" is enabled.
///
/// ## Recommended use cases
/// - Confidential Clients (Web-Server apps) with PKCE and/or client secret.
/// - Public Clients (Web-Client, Mobile and Desktop apps) with PKCE **without client secret**.
/// - Confidential clients (Web-Server apps) with PKCE and/or client secret.
/// - Public clients (Web-Client, Mobile and Desktop apps) with PKCE **without client secret**.
///
/// ## Not recommended use cases
/// - Public Clients (Web-Client, Mobile and Desktop apps) with **client secret**, because secret is no longer secret in public clients.
/// - Public clients (Web-Client, Mobile and Desktop apps) with **client secret**, because secret is no longer secret in public clients.
///
/// ## Not supported use cases
/// - Browserless or input-constrained devices, use Device Code grant type: [`crate::oauth::DeviceCodeClient`] instead.
///
/// ## Example
/// ```
Expand Down
1 change: 1 addition & 0 deletions src/oauth/data.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::oauth::OAuthError;
use crate::oauth::OAuthResult;
use std::collections::HashSet;
use std::env::VarError;

/// The PKCE code challenge option.
Expand Down
18 changes: 7 additions & 11 deletions src/oauth/device_code_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,21 @@ use crate::oauth::VerificationUriComplete;
/// This is only available when the feature "oauth" is enabled.
///
/// ## Recommended use cases
/// - Confidential Clients (Web-Server apps) with client secret.
/// - Public Clients (Web-Client, Mobile and Desktop apps) without client secret.
/// - Browserless or input-constrained devices.
///
/// ## Not recommended use cases
/// - Public Clients (Web-Client, Mobile and Desktop apps) with **client secret**, because secret is not secret in public clients.
/// - Browser-enabled devices, use Authorization Code grant type: [`crate::oauth::AuthorizationCodeClient`] instead.
///
/// ## Example
/// ```
/// use fars::oauth::DeviceCodeClient;
/// use fars::oauth::ClientId;
/// use fars::oauth::ClientSecret;
/// use fars::oauth::DeviceEndpoint;
/// use fars::oauth::TokenEndpoint;
///
/// let client = DeviceCodeClient::new(
/// ClientId::new("client-id"),
/// Some(ClientSecret::new("client-secret")),
/// None,
/// DeviceEndpoint::new("https://example.com/device")?,
/// TokenEndpoint::new("https://example.com/token")?,
/// )?;
Expand All @@ -60,13 +58,12 @@ impl DeviceCodeClient {
/// ```
/// use fars::oauth::DeviceCodeClient;
/// use fars::oauth::ClientId;
/// use fars::oauth::ClientSecret;
/// use fars::oauth::DeviceEndpoint;
/// use fars::oauth::TokenEndpoint;
///
/// let client = DeviceCodeClient::new(
/// ClientId::new("client-id"),
/// Some(ClientSecret::new("client-secret")),
/// None,
/// DeviceEndpoint::new("https://example.com/device")?,
/// TokenEndpoint::new("https://example.com/token")?,
/// )?;
Expand Down Expand Up @@ -117,7 +114,6 @@ impl DeviceCodeClient {
/// use std::collections::HashSet;
/// use fars::oauth::DeviceCodeClient;
/// use fars::oauth::ClientId;
/// use fars::oauth::ClientSecret;
/// use fars::oauth::DeviceEndpoint;
/// use fars::oauth::TokenEndpoint;
/// use fars::oauth::OAuthScope;
Expand All @@ -126,7 +122,7 @@ impl DeviceCodeClient {
/// async fn main() -> anyhow::Result<()> {
/// let client = DeviceCodeClient::new(
/// ClientId::new("client-id"),
/// Some(ClientSecret::new("client-secret")),
/// None,
/// DeviceEndpoint::new("https://example.com/device")?,
/// TokenEndpoint::new("https://example.com/token")?,
/// )?;
Expand All @@ -151,7 +147,7 @@ impl DeviceCodeClient {
let request = self
.client
.exchange_device_code()
.map_err(OAuthError::DeviceAuthorizationFailed)?;
.map_err(OAuthError::DeviceAuthorizationRequestError)?;

// Set scopes.
let request = scopes
Expand All @@ -164,7 +160,7 @@ impl DeviceCodeClient {
let response = request
.request_async(oauth2::reqwest::async_http_client)
.await
.map_err(OAuthError::AuthCodeExchangeTokenFailed)?;
.map_err(OAuthError::DeviceCodeExchangeFailed)?;

Ok(DeviceCodeSession {
verification_uri: VerificationUri {
Expand Down
6 changes: 2 additions & 4 deletions src/oauth/device_code_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use crate::oauth::VerificationUriComplete;
/// use std::collections::HashSet;
/// use fars::oauth::DeviceCodeClient;
/// use fars::oauth::ClientId;
/// use fars::oauth::ClientSecret;
/// use fars::oauth::DeviceEndpoint;
/// use fars::oauth::TokenEndpoint;
/// use fars::oauth::OAuthScope;
Expand All @@ -30,7 +29,7 @@ use crate::oauth::VerificationUriComplete;
/// async fn main() -> anyhow::Result<()> {
/// let client = DeviceCodeClient::new(
/// ClientId::new("client-id"),
/// Some(ClientSecret::new("client-secret")),
/// None,
/// DeviceEndpoint::new("https://example.com/device")?,
/// TokenEndpoint::new("https://example.com/token")?,
/// )?;
Expand Down Expand Up @@ -67,7 +66,6 @@ impl DeviceCodeSession {
/// use std::collections::HashSet;
/// use fars::oauth::DeviceCodeClient;
/// use fars::oauth::ClientId;
/// use fars::oauth::ClientSecret;
/// use fars::oauth::DeviceEndpoint;
/// use fars::oauth::TokenEndpoint;
/// use fars::oauth::OAuthScope;
Expand All @@ -76,7 +74,7 @@ impl DeviceCodeSession {
/// async fn main() -> anyhow::Result<()> {
/// let client = DeviceCodeClient::new(
/// ClientId::new("client-id"),
/// Some(ClientSecret::new("client-secret")),
/// None,
/// DeviceEndpoint::new("https://example.com/device")?,
/// TokenEndpoint::new("https://example.com/token")?,
/// )?;
Expand Down
35 changes: 30 additions & 5 deletions src/oauth/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub enum OAuthError {
#[error("State mismatch")]
StateMismatch,
/// Exchange token failed on the authorization code flow.
#[error("Auth code exchange token failed: {0}")]
#[error("Auth code exchange token failed: {0:?}")]
AuthCodeExchangeTokenFailed(
RequestTokenError<
oauth2::reqwest::Error<reqwest::Error>,
Expand All @@ -31,15 +31,40 @@ pub enum OAuthError {
>,
>,
),
/// Device authorization failed.
#[error("Device authorization failed: {0}")]
DeviceAuthorizationFailed(ConfigurationError),
/// Device authorization request error.
#[error("Device authorization request error: {0:?}")]
DeviceAuthorizationRequestError(ConfigurationError),
/// Exchange device code failed on the device code flow.
#[error("Device code exchange token failed: {0:?}")]
DeviceCodeExchangeFailed(
RequestTokenError<
oauth2::reqwest::Error<reqwest::Error>,
oauth2::StandardErrorResponse<
oauth2::basic::BasicErrorResponseType,
>,
>,
),
/// Exchange token failed on the device code flow.
#[error("Device exchange token failed: {0}")]
#[error("Device exchange token failed: {0:?}")]
DeviceExchangeTokenFailed(
RequestTokenError<
oauth2::reqwest::Error<reqwest::Error>,
oauth2::DeviceCodeErrorResponse,
>,
),
/// Internal reqwest error.
#[error("Internal reqwest error: {0:?}")]
ReqwestError(reqwest::Error),
/// JSON deserialization failed.
#[error("JSON deserialization failed: {0:?}, {1:?}")]
JsonDeserializationFailed(serde_json::Error, String),
/// Manual API call failed.
#[error("Manual API call failed: {0:?}, {1:?}")]
ManualApiCallFailed(reqwest::StatusCode, String),
/// Continue polling.
#[error("Continue polling")]
ContinuePolling,
/// Timeout.
#[error("Timeout")]
Timeout,
}
1 change: 1 addition & 0 deletions src/oauth/idp.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! The OAuth 2.0 client implementations for each identity provider (IdP).

pub(super) mod facebook_auth_code;
pub(super) mod facebook_device_code;
pub(super) mod github_auth_code;
pub(super) mod google_auth_code;
pub(super) mod google_device_code;
Expand Down
4 changes: 2 additions & 2 deletions src/oauth/idp/facebook_auth_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::oauth::TokenEndpoint;
/// This is only available when the feature `oauth` is enabled.
///
/// ## Recommended use cases
/// - Confidential and Public Clients (Web-Server, Web-Client, Mobile and Desktop apps) with PKCE.
/// - Confidential clients (Web-Server apps) and public clients (Web-Client, Mobile and Desktop apps) with PKCE.
///
/// ## Example
/// ```
Expand Down Expand Up @@ -115,7 +115,7 @@ impl FacebookAuthorizationCodeClient {
/// )?;
///
/// let session = client.generate_authorization_session(HashSet::from([
/// OAuthScope::new("email"),
/// OAuthScope::open_id_email(),
/// ]));
///
/// let authorize_url = session.authorize_url.inner();
Expand Down
Loading

0 comments on commit c3163af

Please sign in to comment.