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