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