1#![doc = include_str!("../README.md")]
2
3use std::borrow::Cow;
4
5use bon::bon;
6use oauth2::{
7 Client, CsrfToken, HttpClientError, RequestTokenError, TokenResponse, basic::BasicErrorResponse,
8};
9
10pub mod common;
11mod provider;
12pub mod types;
13
14pub use provider::{SimpleOAuthProvider, UserInfoProvider};
15
16use crate::types::{
17 AuthorizeUrl, OAuthClient, OAuthCredentials, OAuthTokenResponse, StandardTokenResponse,
18 UserInfo,
19};
20
21#[derive(Debug, thiserror::Error)]
22pub enum SimpleOAuthError {
23 #[error(transparent)]
24 Request(#[from] reqwest::Error),
25 #[error("invalid url: {0}")]
26 ParseUrl(#[from] oauth2::url::ParseError),
27 #[error("token exchange error: {0}")]
28 TokenExchange(#[from] RequestTokenError<HttpClientError<reqwest::Error>, BasicErrorResponse>),
29 #[error("deserialization error: {0}")]
30 Deserialization(#[from] serde_json::Error),
31}
32
33#[derive(Debug, Clone)]
34pub struct SimpleOAuthClient<P> {
35 http_client: reqwest::Client,
36 oauth_http_client: oauth2_reqwest::ReqwestClient,
37 oauth_client: OAuthClient,
38 credentials: OAuthCredentials,
39 provider: P,
40}
41
42#[bon]
43impl<P> SimpleOAuthClient<P>
44where
45 P: SimpleOAuthProvider,
46{
47 #[builder(on(String, into))]
48 #[builder(on(OAuthCredentials, into))]
49 pub fn new(
50 provider: P,
51 credentials: OAuthCredentials,
52 redirect_url: String,
53 http_client: Option<&reqwest::Client>,
54 ) -> Result<Self, SimpleOAuthError> {
55 let http_client = if let Some(client) = http_client {
56 client.to_owned()
57 } else {
58 reqwest::Client::builder()
59 .redirect(reqwest::redirect::Policy::none())
60 .build()?
61 };
62 let oauth_client = Client::new(oauth2::ClientId::new(credentials.client_id.clone()))
63 .set_client_secret(oauth2::ClientSecret::new(credentials.client_secret.clone()))
64 .set_redirect_uri(oauth2::RedirectUrl::new(redirect_url)?)
65 .set_auth_uri(oauth2::AuthUrl::new(provider.authorize_url().into())?)
66 .set_token_uri(oauth2::TokenUrl::new(provider.token_url().into())?)
67 .set_auth_type(provider.token_auth_method());
68
69 Ok(Self {
70 oauth_http_client: oauth2_reqwest::ReqwestClient::from(http_client.clone()),
71 http_client,
72 oauth_client,
73 credentials,
74 provider,
75 })
76 }
77
78 #[builder(on(String, into), finish_fn(name = "build"))]
86 pub fn authorize_url(
87 &self,
88 redirect_url: Option<String>,
89 scopes: Option<&[&str]>,
90 ) -> Result<AuthorizeUrl, SimpleOAuthError> {
91 let (pkce_challenge, pkce_verifier) = oauth2::PkceCodeChallenge::new_random_sha256();
92 let mut auth_request = self
93 .oauth_client
94 .authorize_url(CsrfToken::new_random)
95 .set_pkce_challenge(pkce_challenge)
96 .add_scopes(
97 scopes
98 .unwrap_or(self.provider.default_scopes())
99 .iter()
100 .map(|s| oauth2::Scope::new((*s).to_owned())),
101 );
102 if let Some(redirect_url) = redirect_url {
103 auth_request =
104 auth_request.set_redirect_uri(Cow::Owned(oauth2::RedirectUrl::new(redirect_url)?));
105 }
106 let (url, state) = auth_request.url();
107
108 Ok(AuthorizeUrl {
109 url,
110 state: state.into_secret(),
111 pkce_verifier: pkce_verifier.into_secret(),
112 })
113 }
114
115 #[builder(on(String, into), finish_fn(name = "build"))]
120 pub async fn exchange_code(
121 &self,
122 code: String,
123 pkce_verifier: String,
124 redirect_url: Option<String>,
125 ) -> Result<StandardTokenResponse, SimpleOAuthError> {
126 let mut token_request = self
127 .oauth_client
128 .exchange_code(oauth2::AuthorizationCode::new(code))
129 .set_pkce_verifier(oauth2::PkceCodeVerifier::new(pkce_verifier));
130 if let Some(redirect_url) = redirect_url {
131 token_request =
132 token_request.set_redirect_uri(Cow::Owned(oauth2::RedirectUrl::new(redirect_url)?));
133 }
134 let token = token_request.request_async(&self.oauth_http_client).await?;
135
136 Ok(standard_token_response(token))
137 }
138
139 #[builder(on(String, into), finish_fn(name = "build"))]
141 pub async fn exchange_refresh_token(
142 &self,
143 refresh_token: String,
144 ) -> Result<StandardTokenResponse, SimpleOAuthError> {
145 let token = self
146 .oauth_client
147 .exchange_refresh_token(&oauth2::RefreshToken::new(refresh_token))
148 .request_async(&self.oauth_http_client)
149 .await?;
150
151 Ok(standard_token_response(token))
152 }
153}
154
155fn standard_token_response(token: OAuthTokenResponse) -> StandardTokenResponse {
156 StandardTokenResponse {
157 access_token: token.access_token().secret().to_owned(),
158 refresh_token: token.refresh_token().map(|s| s.secret().to_owned()),
159 expires_in: token.expires_in(),
160 id_token: token.extra_fields().id_token.clone(),
161 }
162}
163
164impl<P> SimpleOAuthClient<P>
165where
166 P: UserInfoProvider,
167{
168 pub async fn get_user_info(&self, access_token: &str) -> Result<UserInfo, SimpleOAuthError> {
171 let mut user_info_request = self
172 .http_client
173 .get(self.provider.user_info_url())
174 .bearer_auth(access_token);
175 for (name, val) in self.provider.user_info_headers(&self.credentials) {
176 user_info_request = user_info_request.header(name, val);
177 }
178
179 let user_info_val = user_info_request
180 .send()
181 .await?
182 .error_for_status()?
183 .json()
184 .await?;
185 let user_info = self.provider.extract_user_info(user_info_val)?;
186
187 Ok(user_info)
188 }
189}