use crate::error::Result;
use crate::{AuthAction, AuthConfig, AuthUrlProvider};
use async_trait::async_trait;
use reqwest::header::ACCEPT;
use serde::{Deserialize, Serialize};
use serde_with::{formats::SpaceSeparator, serde_as, StringWithSeparator};
pub struct AuthorizationServer {
config: AuthConfig,
}
impl AuthUrlProvider for AuthorizationServer {
type AuthRequest = AuthRequest;
type TokenRequest = GetTokenRequest;
type UserInfoRequest = GetUserInfoRequest;
fn authorize_url(request: Self::AuthRequest) -> Result<String> {
let query = serde_urlencoded::to_string(request)?;
Ok(format!("https://github.com/login/oauth/authorize?{query}"))
}
fn access_token_url(request: Self::TokenRequest) -> Result<String> {
let query = serde_urlencoded::to_string(request)?;
Ok(format!(
"https://github.com/login/oauth/access_token?token_type=bearer&{query}"
))
}
fn user_info_url(_request: Self::UserInfoRequest) -> Result<String> {
Ok(format!("https://api.github.com/user"))
}
}
#[async_trait]
impl AuthAction for AuthorizationServer {
type AuthCallback = AuthCallback;
type AuthToken = TokenResponse;
type AuthUser = UserInfoResponse;
async fn authorize<S: Into<String> + Send>(&self, state: S) -> Result<String> {
let AuthConfig {
client_id,
redirect_uri,
scope,
..
} = &self.config;
Self::authorize_url(AuthRequest {
client_id: client_id.to_string(),
redirect_uri: redirect_uri.to_string(),
state: state.into(),
scope: scope
.clone()
.or_else(|| Some(vec!["read:user".into(), "user:email".into()]))
.expect("scope is empty"),
..Default::default()
})
}
async fn get_access_token(&self, callback: Self::AuthCallback) -> Result<Self::AuthToken> {
let AuthConfig {
client_id,
client_secret,
redirect_uri,
..
} = &self.config;
let access_token_url = Self::access_token_url(GetTokenRequest {
client_id: client_id.to_string(),
client_secret: client_secret.clone().expect("client_secret is empty"),
code: callback.code,
redirect_uri: redirect_uri.clone(),
})?;
Ok(reqwest::Client::default()
.get(access_token_url)
.header(ACCEPT, "application/json")
.send()
.await?
.json()
.await?)
}
async fn get_user_info(&self, token: Self::AuthToken) -> Result<Self::AuthUser> {
let user_info_url = Self::user_info_url(GetUserInfoRequest {})?;
Ok(reqwest::Client::default()
.get(user_info_url)
.bearer_auth(token.access_token)
.send()
.await?
.json()
.await?)
}
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AuthRequest {
client_id: String,
redirect_uri: String,
login: Option<String>,
#[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
scope: Vec<String>,
state: String,
allow_signup: Option<String>,
prompt: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthCallback {
code: String,
state: String,
}
#[derive(Debug, Serialize)]
pub struct GetTokenRequest {
client_id: String,
client_secret: String,
code: String,
redirect_uri: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TokenResponse {
pub access_token: String,
pub scope: String,
pub token_type: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GetUserInfoRequest {}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UserInfoResponse {
pub login: String,
pub id: i64,
pub node_id: String,
pub avatar_url: String,
pub gravatar_id: String,
pub url: String,
pub html_url: String,
pub followers_url: String,
pub following_url: String,
pub gists_url: String,
pub starred_url: String,
pub subscriptions_url: String,
pub organizations_url: String,
pub repos_url: String,
pub events_url: String,
pub received_events_url: String,
#[serde(rename = "type")]
pub type_field: String,
pub site_admin: bool,
pub name: String,
pub company: String,
pub blog: String,
pub location: String,
pub email: String,
pub hireable: bool,
pub bio: String,
pub twitter_username: String,
pub public_repos: i64,
pub public_gists: i64,
pub followers: i64,
pub following: i64,
pub created_at: String,
pub updated_at: String,
pub private_gists: i64,
pub total_private_repos: i64,
pub owned_private_repos: i64,
pub disk_usage: i64,
pub collaborators: i64,
pub two_factor_authentication: bool,
}