1mod extract;
9mod events;
10mod passport;
11
12#[cfg(feature = "jwt")]
13mod jwt;
14
15#[cfg(feature = "jwt")]
16mod api_token;
17#[cfg(feature = "jwt")]
18mod entity;
19#[cfg(feature = "jwt")]
20mod jwt_auth;
21#[cfg(feature = "jwt")]
22mod migration;
23#[cfg(feature = "jwt")]
24mod password;
25#[cfg(feature = "jwt")]
26mod store;
27
28#[cfg(feature = "oauth")]
29mod oauth;
30
31pub use extract::{Credentials, Extract, Source};
32pub use events::ApiTokenRevoked;
33pub use passport::{passport_serialize, Authenticated, Passport, PassportExt};
34
35#[cfg(feature = "jwt")]
36pub use jwt::{Claims, Jwt, JwtError};
37
38#[cfg(feature = "jwt")]
39pub use jwt_auth::{JwtAuth, JwtAuthExt, JwtAuthState};
40#[cfg(feature = "jwt")]
41pub use api_token::{
42 create_api_token, list_api_tokens, revoke_api_token, token_can, user_for_api_token,
43 ApiTokenInfo, ApiTokenRow, CreateApiToken, CreatedApiToken, PAT_PREFIX,
44};
45#[cfg(feature = "jwt")]
46pub use migration::AuthMigrator;
47#[cfg(feature = "jwt")]
48pub use password::{hash_password, verify_password};
49#[cfg(feature = "jwt")]
50pub use store::{
51 hash_refresh_token, hash_token, issue_token_pair, register_user, AuthUser, TokenPair,
52};
53
54#[cfg(feature = "oauth")]
55pub use oauth::{
56 drivers as oauth_drivers, test_support as oauth_test_support, Oauth, OauthProfile,
57 OauthProvider, OauthTokens, ProfileKind,
58};
59#[cfg(feature = "oauth")]
60pub use oauth::drivers::{Apple, Custom, Driver, Github, Google};
61
62use extract::Extract as ExtractChain;
63use sova_core::extend::{named, BoxFuture, MwEntry};
64use sova_core::{with_state, App, Error, Plugin, Request, Result};
65use std::future::Future;
66use std::marker::PhantomData;
67use std::sync::Arc;
68
69type VerifyFn<U> =
70 Arc<dyn Fn(Credentials, &Request) -> BoxFuture<Result<Option<U>>> + Send + Sync>;
71
72pub struct AuthBuilder {
74 extract: ExtractChain,
75 skip: Vec<String>,
76 optional: bool,
77 name: String,
78}
79
80pub struct AuthMw<U> {
82 extract: ExtractChain,
83 verify: VerifyFn<U>,
84 skip: Vec<String>,
85 optional: bool,
86 name: String,
87 _marker: PhantomData<U>,
88}
89
90impl<U: Send + Sync + 'static> AuthMw<U> {
91 pub fn skip(mut self, path: impl Into<String>) -> Self {
92 self.skip.push(path.into());
93 self
94 }
95
96 pub fn optional(mut self, on: bool) -> Self {
97 self.optional = on;
98 self
99 }
100
101 pub fn name(mut self, name: impl Into<String>) -> Self {
102 self.name = name.into();
103 self
104 }
105
106 pub fn middleware(self) -> MwEntry {
108 let name = self.name.clone();
109 named(
110 name,
111 with_state(self, |auth, mut req, next| async move {
112 if path_skipped(&req.path, &auth.skip) {
113 return next(req).await;
114 }
115
116 let Some(creds) = auth.extract.run(&req) else {
117 if auth.optional {
118 return next(req).await;
119 }
120 return Error::Unauthorized.into_response();
121 };
122
123 match (auth.verify)(creds, &req).await {
124 Ok(Some(user)) => {
125 req.set(Authenticated {
126 id: std::any::type_name::<U>().into(),
127 });
128 req.set(user);
129 next(req).await
130 }
131 Ok(None) => {
132 if auth.optional {
133 next(req).await
134 } else {
135 Error::Unauthorized.into_response()
136 }
137 }
138 Err(err) => err.into_response(),
139 }
140 }),
141 )
142 }
143}
144
145impl AuthBuilder {
146 pub fn new() -> Self {
147 Self {
148 extract: ExtractChain::bearer(),
149 skip: Vec::new(),
150 optional: false,
151 name: "passport-strategy".into(),
152 }
153 }
154
155 pub fn extract(mut self, extract: ExtractChain) -> Self {
156 self.extract = extract;
157 self
158 }
159
160 pub fn skip(mut self, path: impl Into<String>) -> Self {
161 self.skip.push(path.into());
162 self
163 }
164
165 pub fn optional(mut self, on: bool) -> Self {
166 self.optional = on;
167 self
168 }
169
170 pub fn name(mut self, name: impl Into<String>) -> Self {
171 self.name = name.into();
172 self
173 }
174
175 pub fn verify<U, F, Fut>(self, f: F) -> AuthMw<U>
176 where
177 U: Send + Sync + 'static,
178 F: Fn(Credentials, &Request) -> Fut + Send + Sync + 'static,
179 Fut: Future<Output = Result<Option<U>>> + Send + 'static,
180 {
181 self.verify_boxed(move |creds, req| Box::pin(f(creds, req)))
182 }
183
184 pub fn verify_boxed<U, F>(self, f: F) -> AuthMw<U>
185 where
186 U: Send + Sync + 'static,
187 F: Fn(Credentials, &Request) -> BoxFuture<Result<Option<U>>> + Send + Sync + 'static,
188 {
189 AuthMw {
190 extract: self.extract,
191 verify: Arc::new(f),
192 skip: self.skip,
193 optional: self.optional,
194 name: self.name,
195 _marker: PhantomData,
196 }
197 }
198}
199
200impl Default for AuthBuilder {
201 fn default() -> Self {
202 Self::new()
203 }
204}
205
206pub struct Auth;
208
209impl Auth {
210 #[allow(clippy::new_ret_no_self)]
211 pub fn new() -> AuthBuilder {
212 AuthBuilder::new()
213 }
214
215 pub fn bearer<U, F, Fut>(f: F) -> AuthMw<U>
216 where
217 U: Send + Sync + 'static,
218 F: Fn(String, &Request) -> Fut + Send + Sync + 'static,
219 Fut: Future<Output = Result<Option<U>>> + Send + 'static,
220 {
221 Auth::new()
222 .name("bearer")
223 .extract(ExtractChain::bearer())
224 .verify(move |creds, req| f(creds.value().to_string(), req))
225 }
226
227 pub fn api_key<U, F, Fut>(header: impl Into<String>, f: F) -> AuthMw<U>
228 where
229 U: Send + Sync + 'static,
230 F: Fn(String, &Request) -> Fut + Send + Sync + 'static,
231 Fut: Future<Output = Result<Option<U>>> + Send + 'static,
232 {
233 Auth::new()
234 .name("api-key")
235 .extract(ExtractChain::header(header))
236 .verify(move |creds, req| f(creds.value().to_string(), req))
237 }
238
239 #[cfg(feature = "jwt")]
240 pub fn jwt_hs256<U, C, F, Fut>(secret: impl Into<String>, f: F) -> AuthMw<U>
241 where
242 U: Send + Sync + 'static,
243 C: serde::de::DeserializeOwned + Send + 'static,
244 F: Fn(C, &Request) -> Fut + Send + Sync + 'static,
245 Fut: Future<Output = Result<Option<U>>> + Send + 'static,
246 {
247 let jwt = Jwt::hs256(secret);
248 Auth::new()
249 .name("jwt")
250 .extract(ExtractChain::bearer())
251 .verify_boxed(move |creds, req| match jwt.decode::<C>(creds.value()) {
252 Ok(claims) => Box::pin(f(claims, req)),
253 Err(_) => Box::pin(async { Ok(None) }),
254 })
255 }
256}
257
258pub fn local_strategy<U, F, Fut>(f: F) -> MwEntry
260where
261 U: Send + Sync + 'static,
262 F: Fn(String, String, Request) -> Fut + Send + Sync + 'static,
263 Fut: Future<Output = Result<(Option<U>, Request)>> + Send + 'static,
264{
265 let f = Arc::new(f);
266 named(
267 "local",
268 with_state(f, |f, mut req, next| async move {
269 #[derive(serde::Deserialize)]
270 struct Body {
271 email: String,
272 password: String,
273 }
274 let body: Body = match req.json().await {
275 Ok(b) => b,
276 Err(e) => return e.into_response(),
277 };
278 match f(body.email, body.password, req).await {
279 Ok((Some(user), mut req)) => {
280 req.set(Authenticated {
281 id: std::any::type_name::<U>().into(),
282 });
283 req.set(user);
284 next(req).await
285 }
286 Ok((None, _)) => Error::Unauthorized.into_response(),
287 Err(e) => e.into_response(),
288 }
289 }),
290 )
291}
292
293impl<U: Send + Sync + 'static> Plugin for AuthMw<U> {
294 fn id(&self) -> &'static str {
295 "passport-auth-mw"
296 }
297
298 fn meta(&self) -> sova_core::PluginMeta {
299 sova_core::PluginMeta::new("Passport strategy")
300 .description("Extract + verify authentication strategy")
301 .version(env!("CARGO_PKG_VERSION"))
302 }
303
304 fn install(self, app: &mut App) {
305 app.use_middleware(self.middleware());
306 }
307}
308
309fn path_skipped(path: &str, skips: &[String]) -> bool {
310 skips.iter().any(|p| {
311 if let Some(prefix) = p.strip_suffix('*') {
312 path.starts_with(prefix)
313 } else {
314 path == p
315 }
316 })
317}
318
319#[doc(inline)]
321pub use PassportExt as AuthExt;