Skip to main content

simple_oauth/
lib.rs

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    /// Build the URL to navigate the user to for authorization. **Make sure to save the returned state and
79    /// PKCE verifier in a secure location, typically in a server-side cache or session.**
80    ///
81    /// If scopes are not provided, this will use the provider's default scopes.
82    ///
83    /// You can optionally override the redirect URL, but make sure to pass in the exact same URL when calling
84    /// `exchange_code()`.
85    #[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    /// Exchange the returned code after authorization for an access/refresh token. You will need to provide
116    /// the returned code and the saved PKCE verifier. Make sure to first verify the returned state if applicable.
117    ///
118    /// If you set the redirect URL when calling `authorize_url()`, you must set the same URL here as well.
119    #[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    /// Exchange the refresh token for a new access token
140    #[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    /// Retrieve user info from the provider using the access token. This is a convenience
169    /// method for providers that support normalized user info (e.g. id, name, email, avatar).
170    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}