1use std::fmt;
2use std::future::Future;
3use std::net::IpAddr;
4use std::sync::Arc;
5use std::time::Duration;
6
7use axum::http::HeaderMap;
8
9use crate::adapter::Adapter;
10use crate::client_ip::TrustedProxies;
11use crate::delivery::{Delivery, DeliveryError, DeliveryFuture};
12
13const MIN_SECRET_LENGTH: usize = 32;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum SameSite {
18 Strict,
19 Lax,
20 None,
21}
22
23impl SameSite {
24 pub(crate) fn as_str(self) -> &'static str {
25 match self {
26 SameSite::Strict => "Strict",
27 SameSite::Lax => "Lax",
28 SameSite::None => "None",
29 }
30 }
31}
32
33pub(crate) type DeliverFn = Arc<dyn Fn(Delivery) -> DeliveryFuture + Send + Sync>;
34pub(crate) type ClientIpFn =
35 Arc<dyn Fn(&HeaderMap, Option<IpAddr>) -> Option<IpAddr> + Send + Sync>;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39#[non_exhaustive]
40pub enum BuildError {
41 InvalidAuthServerUrl,
43 WeakCookieSecret,
45 WeakServiceSecret,
47 InvalidCookieAttribute(String),
49 HttpClient(String),
51}
52
53impl fmt::Display for BuildError {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 match self {
56 BuildError::InvalidAuthServerUrl => {
57 f.write_str("seamless-auth: the auth server URL must be an http or https URL")
58 }
59 BuildError::WeakCookieSecret => write!(
60 f,
61 "seamless-auth: the cookie secret must be at least {MIN_SECRET_LENGTH} bytes"
62 ),
63 BuildError::WeakServiceSecret => write!(
64 f,
65 "seamless-auth: the service secret must be at least {MIN_SECRET_LENGTH} bytes"
66 ),
67 BuildError::InvalidCookieAttribute(what) => {
68 write!(f, "seamless-auth: invalid cookie attribute {what}")
69 }
70 BuildError::HttpClient(err) => {
71 write!(f, "seamless-auth: could not build the HTTP client: {err}")
72 }
73 }
74 }
75}
76
77impl std::error::Error for BuildError {}
78
79pub struct Builder {
81 auth_server_url: String,
82 auth_server_issuer: Option<String>,
83 audience: Option<String>,
84 cookie_secret: String,
85 service_secret: String,
86 jwks_kid: Option<String>,
87 cookie_domain: Option<String>,
88 insecure_cookies: bool,
89 same_site: Option<SameSite>,
90 allowed_origins: Vec<String>,
91 access_cookie_name: String,
92 registration_cookie_name: String,
93 refresh_cookie_name: String,
94 pre_auth_cookie_name: String,
95 deliver: Option<DeliverFn>,
96 client_ip: Option<ClientIpFn>,
97 disable_manifest_fetch: bool,
98 http_client: Option<reqwest::Client>,
99}
100
101impl fmt::Debug for Builder {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.debug_struct("Builder")
104 .field("auth_server_url", &self.auth_server_url)
105 .field("auth_server_issuer", &self.auth_server_issuer)
106 .finish_non_exhaustive()
107 }
108}
109
110impl Builder {
111 pub(crate) fn new(auth_server_url: impl Into<String>) -> Self {
112 Builder {
113 auth_server_url: auth_server_url.into(),
114 auth_server_issuer: None,
115 audience: None,
116 cookie_secret: String::new(),
117 service_secret: String::new(),
118 jwks_kid: None,
119 cookie_domain: None,
120 insecure_cookies: false,
121 same_site: None,
122 allowed_origins: Vec::new(),
123 access_cookie_name: "seamless-access".into(),
124 registration_cookie_name: "seamless-ephemeral".into(),
125 refresh_cookie_name: "seamless-refresh".into(),
126 pre_auth_cookie_name: "seamless-ephemeral".into(),
129 deliver: None,
130 client_ip: None,
131 disable_manifest_fetch: false,
132 http_client: None,
133 }
134 }
135
136 pub fn auth_server_issuer(mut self, issuer: impl Into<String>) -> Self {
140 self.auth_server_issuer = Some(issuer.into());
141 self
142 }
143
144 pub fn audience(mut self, audience: impl Into<String>) -> Self {
147 self.audience = Some(audience.into());
148 self
149 }
150
151 pub fn cookie_secret(mut self, secret: impl Into<String>) -> Self {
153 self.cookie_secret = secret.into();
154 self
155 }
156
157 pub fn service_secret(mut self, secret: impl Into<String>) -> Self {
161 self.service_secret = secret.into();
162 self
163 }
164
165 pub fn jwks_kid(mut self, kid: impl Into<String>) -> Self {
167 self.jwks_kid = Some(kid.into());
168 self
169 }
170
171 pub fn cookie_domain(mut self, domain: impl Into<String>) -> Self {
173 self.cookie_domain = Some(domain.into());
174 self
175 }
176
177 pub fn insecure_cookies(mut self, insecure: bool) -> Self {
179 self.insecure_cookies = insecure;
180 self
181 }
182
183 pub fn same_site(mut self, same_site: SameSite) -> Self {
186 self.same_site = Some(same_site);
187 self
188 }
189
190 pub fn allowed_origins<I, S>(mut self, origins: I) -> Self
193 where
194 I: IntoIterator<Item = S>,
195 S: Into<String>,
196 {
197 self.allowed_origins = origins.into_iter().map(Into::into).collect();
198 self
199 }
200
201 pub fn session_cookie_names(
203 mut self,
204 access: impl Into<String>,
205 refresh: impl Into<String>,
206 ) -> Self {
207 self.access_cookie_name = access.into();
208 self.refresh_cookie_name = refresh.into();
209 self
210 }
211
212 pub fn flow_cookie_names(
214 mut self,
215 registration: impl Into<String>,
216 pre_auth: impl Into<String>,
217 ) -> Self {
218 self.registration_cookie_name = registration.into();
219 self.pre_auth_cookie_name = pre_auth.into();
220 self
221 }
222
223 pub fn deliver<F, Fut, E>(mut self, deliver: F) -> Self
228 where
229 F: Fn(Delivery) -> Fut + Send + Sync + 'static,
230 Fut: Future<Output = Result<(), E>> + Send + 'static,
231 E: Into<DeliveryError>,
232 {
233 self.deliver = Some(Arc::new(move |delivery| {
234 let fut = deliver(delivery);
235 Box::pin(async move { fut.await.map_err(Into::into) })
236 }));
237 self
238 }
239
240 pub fn resolve_client_ip<F>(mut self, resolve: F) -> Self
244 where
245 F: Fn(&HeaderMap, Option<IpAddr>) -> Option<IpAddr> + Send + Sync + 'static,
246 {
247 self.client_ip = Some(Arc::new(resolve));
248 self
249 }
250
251 pub fn trusted_proxies(self, proxies: TrustedProxies) -> Self {
253 self.resolve_client_ip(move |headers, peer| proxies.resolve(headers, peer))
254 }
255
256 pub fn disable_manifest_fetch(mut self, disable: bool) -> Self {
259 self.disable_manifest_fetch = disable;
260 self
261 }
262
263 pub fn http_client(mut self, client: reqwest::Client) -> Self {
267 self.http_client = Some(client);
268 self
269 }
270
271 pub fn build(self) -> Result<Adapter, BuildError> {
273 let auth_server_url = self
274 .auth_server_url
275 .trim()
276 .trim_end_matches('/')
277 .to_string();
278 if !(auth_server_url.starts_with("http://") || auth_server_url.starts_with("https://"))
279 || auth_server_url.len() <= "https://".len()
280 {
281 return Err(BuildError::InvalidAuthServerUrl);
282 }
283 if self.cookie_secret.len() < MIN_SECRET_LENGTH {
284 return Err(BuildError::WeakCookieSecret);
285 }
286 if self.service_secret.len() < MIN_SECRET_LENGTH {
287 return Err(BuildError::WeakServiceSecret);
288 }
289
290 for name in [
291 &self.access_cookie_name,
292 &self.registration_cookie_name,
293 &self.refresh_cookie_name,
294 &self.pre_auth_cookie_name,
295 ] {
296 if name.is_empty() || !name.bytes().all(is_token_byte) {
297 return Err(BuildError::InvalidCookieAttribute(format!("name {name:?}")));
298 }
299 }
300 if let Some(domain) = &self.cookie_domain
301 && (domain.is_empty()
302 || !domain
303 .bytes()
304 .all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-'))
305 {
306 return Err(BuildError::InvalidCookieAttribute(format!(
307 "domain {domain:?}"
308 )));
309 }
310
311 let auth_server_issuer = self
312 .auth_server_issuer
313 .unwrap_or_else(|| auth_server_url.clone());
314 let audience = self.audience.unwrap_or_else(|| auth_server_issuer.clone());
315 let jwks_kid = self.jwks_kid.filter(|k| !k.is_empty()).unwrap_or_else(|| {
316 tracing::warn!(
317 "[seamless-auth] jwks_kid is not set and defaults to \"dev-main\", a placeholder. Set it to name the service-token key explicitly."
318 );
319 "dev-main".to_string()
320 });
321 let same_site = self.same_site.unwrap_or(if self.insecure_cookies {
322 SameSite::Lax
323 } else {
324 SameSite::None
325 });
326
327 let http_client = match self.http_client {
328 Some(client) => client,
329 None => reqwest::Client::builder()
330 .timeout(Duration::from_secs(15))
331 .redirect(reqwest::redirect::Policy::none())
332 .build()
333 .map_err(|e| BuildError::HttpClient(e.to_string()))?,
334 };
335
336 Ok(Adapter::from_config(Config {
337 auth_server_url,
338 auth_server_issuer,
339 audience,
340 cookie_secret: self.cookie_secret,
341 service_secret: self.service_secret,
342 jwks_kid,
343 cookie_domain: self.cookie_domain,
344 secure_cookies: !self.insecure_cookies,
345 same_site,
346 allowed_origins: self.allowed_origins,
347 access_cookie_name: self.access_cookie_name,
348 registration_cookie_name: self.registration_cookie_name,
349 refresh_cookie_name: self.refresh_cookie_name,
350 pre_auth_cookie_name: self.pre_auth_cookie_name,
351 deliver: self.deliver,
352 client_ip: self.client_ip,
353 fetch_manifest: !self.disable_manifest_fetch,
354 http_client,
355 }))
356 }
357}
358
359fn is_token_byte(b: u8) -> bool {
361 b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b)
362}
363
364pub(crate) struct Config {
365 pub auth_server_url: String,
366 pub auth_server_issuer: String,
367 pub audience: String,
368 pub cookie_secret: String,
369 pub service_secret: String,
370 pub jwks_kid: String,
371 pub cookie_domain: Option<String>,
372 pub secure_cookies: bool,
373 pub same_site: SameSite,
374 pub allowed_origins: Vec<String>,
375 pub access_cookie_name: String,
376 pub registration_cookie_name: String,
377 pub refresh_cookie_name: String,
378 pub pre_auth_cookie_name: String,
379 pub deliver: Option<DeliverFn>,
380 pub client_ip: Option<ClientIpFn>,
381 pub fetch_manifest: bool,
382 pub http_client: reqwest::Client,
383}
384
385impl Config {
386 pub fn cookie_name(&self, held: &str) -> &str {
388 match held {
389 "access" => &self.access_cookie_name,
390 "registration" => &self.registration_cookie_name,
391 "refresh" => &self.refresh_cookie_name,
392 _ => &self.pre_auth_cookie_name,
393 }
394 }
395}