axum_security_oauth2/
builder.rs1use std::{error::Error as StdError, fmt};
2
3use url::Url;
4
5use crate::{
6 client::{AuthType, OAuth2Client},
7 http::HttpClient,
8};
9
10pub struct OAuth2ClientBuilder {
19 client_id: Option<String>,
20 client_secret: Option<String>,
21 auth_url: Option<String>,
22 token_url: Option<String>,
23 redirect_url: Option<String>,
24 scopes: Vec<String>,
25 auth_type: AuthType,
26 http: Option<HttpClient>,
27}
28
29impl OAuth2ClientBuilder {
30 pub(crate) fn new() -> Self {
31 Self {
32 client_id: None,
33 client_secret: None,
34 auth_url: None,
35 token_url: None,
36 redirect_url: None,
37 scopes: Vec::new(),
38 auth_type: AuthType::default(),
39 http: None,
40 }
41 }
42
43 pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
45 self.client_id = Some(client_id.into());
46 self
47 }
48
49 pub fn client_secret(mut self, client_secret: impl Into<String>) -> Self {
53 self.client_secret = Some(client_secret.into());
54 self
55 }
56
57 pub fn auth_url(mut self, auth_url: impl Into<String>) -> Self {
60 self.auth_url = Some(auth_url.into());
61 self
62 }
63
64 pub fn token_url(mut self, token_url: impl Into<String>) -> Self {
66 self.token_url = Some(token_url.into());
67 self
68 }
69
70 pub fn redirect_url(mut self, redirect_url: impl Into<String>) -> Self {
73 self.redirect_url = Some(redirect_url.into());
74 self
75 }
76
77 pub fn scopes(mut self, scopes: &[&str]) -> Self {
80 self.scopes = scopes.iter().map(|scope| scope.to_string()).collect();
81 self
82 }
83
84 pub fn basic_auth(mut self) -> Self {
88 self.auth_type = AuthType::BasicAuth;
89 self
90 }
91
92 pub fn request_body(mut self) -> Self {
95 self.auth_type = AuthType::RequestBody;
96 self
97 }
98
99 #[cfg_attr(not(feature = "reqwest"), allow(unreachable_code, unused_mut))]
107 pub fn http_client(mut self, http_client: impl Into<HttpClient>) -> Self {
108 self.http = Some(http_client.into());
109 self
110 }
111
112 pub fn set_client_id(&mut self, client_id: impl Into<String>) {
114 self.client_id = Some(client_id.into());
115 }
116
117 pub fn set_client_secret(&mut self, client_secret: impl Into<String>) {
120 self.client_secret = Some(client_secret.into());
121 }
122
123 pub fn set_auth_url(&mut self, auth_url: impl Into<String>) {
126 self.auth_url = Some(auth_url.into());
127 }
128
129 pub fn set_token_url(&mut self, token_url: impl Into<String>) {
131 self.token_url = Some(token_url.into());
132 }
133
134 pub fn set_redirect_url(&mut self, redirect_url: impl Into<String>) {
137 self.redirect_url = Some(redirect_url.into());
138 }
139
140 pub fn set_scopes(&mut self, scopes: &[&str]) {
142 self.scopes = scopes.iter().map(|scope| scope.to_string()).collect();
143 }
144
145 #[cfg_attr(not(feature = "reqwest"), allow(unreachable_code))]
148 pub fn set_http_client(&mut self, http_client: impl Into<HttpClient>) {
149 self.http = Some(http_client.into());
150 }
151
152 pub fn build(self) -> OAuth2Client {
157 self.try_build().unwrap()
158 }
159
160 pub fn try_build(self) -> Result<OAuth2Client, ConfigError> {
162 let client_id = self.client_id.ok_or(ConfigError::MissingClientId)?;
163
164 let auth_url = Url::parse(&self.auth_url.ok_or(ConfigError::MissingAuthUrl)?)
165 .map_err(ConfigError::InvalidAuthUrl)?;
166
167 let token_url = Url::parse(&self.token_url.ok_or(ConfigError::MissingTokenUrl)?)
168 .map_err(ConfigError::InvalidTokenUrl)?;
169
170 let redirect_url = self
171 .redirect_url
172 .map(|url| Url::parse(&url))
173 .transpose()
174 .map_err(ConfigError::InvalidRedirectUrl)?;
175
176 let http = self.http;
177 #[cfg(feature = "reqwest")]
178 let http = http.or_else(|| {
179 Some(HttpClient::Reqwest(
180 crate::http::dep_reqwest::default_client(),
181 ))
182 });
183 let http = http.ok_or(ConfigError::NoHttpClient)?;
184
185 Ok(OAuth2Client {
186 client_id,
187 client_secret: self.client_secret,
188 auth_url,
189 token_url,
190 redirect_url,
191 scopes: self.scopes,
192 auth_type: self.auth_type,
193 http,
194 })
195 }
196}
197
198#[derive(Debug)]
200#[non_exhaustive]
201pub enum ConfigError {
202 MissingClientId,
203 MissingAuthUrl,
204 MissingTokenUrl,
205 InvalidAuthUrl(url::ParseError),
206 InvalidTokenUrl(url::ParseError),
207 InvalidRedirectUrl(url::ParseError),
208 NoHttpClient,
211}
212
213impl fmt::Display for ConfigError {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 match self {
216 ConfigError::MissingClientId => f.write_str("client id is missing"),
217 ConfigError::MissingAuthUrl => f.write_str("authorization url is missing"),
218 ConfigError::MissingTokenUrl => f.write_str("token url is missing"),
219 ConfigError::InvalidAuthUrl(parse_error) => {
220 write!(f, "could not parse authorization url: {parse_error}")
221 }
222 ConfigError::InvalidTokenUrl(parse_error) => {
223 write!(f, "could not parse token url: {parse_error}")
224 }
225 ConfigError::InvalidRedirectUrl(parse_error) => {
226 write!(f, "could not parse redirect url: {parse_error}")
227 }
228 ConfigError::NoHttpClient => f.write_str(
229 "no HTTP client available (enable a backend feature such as `reqwest`, or set one with `http_client`)",
230 ),
231 }
232 }
233}
234
235impl StdError for ConfigError {}
236
237#[cfg(all(test, not(feature = "reqwest")))]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn no_backend_is_a_config_error() {
245 let result = OAuth2ClientBuilder::new()
246 .client_id("id")
247 .auth_url("https://provider.example/authorize")
248 .token_url("https://provider.example/token")
249 .try_build();
250 assert!(matches!(result, Err(ConfigError::NoHttpClient)));
251 }
252}