dioxus_clerk/server/layer.rs
1//! Non-rejecting tower layer that records request credential verification
2//! outcomes for downstream handlers / server functions to gate on.
3//!
4//! This layer never short-circuits with 401: when no token is present (or the
5//! token fails to validate), the request
6//! is forwarded with a [`VerificationOutcome`](crate::core::VerificationOutcome) extension describing the
7//! result. Server functions should use the context reader exposed by
8//! [`crate::server::current_auth`] / [`crate::server::current_auth_opt`]; handlers that need lower-level access can
9//! inspect `VerificationOutcome` directly. This lets public and private routes
10//! coexist under the same layer.
11
12use std::{
13 future::Future,
14 pin::Pin,
15 sync::Arc,
16 task::{Context, Poll},
17};
18
19use super::config::ClerkAuthLayerConfig;
20use super::verification::{VerifiedRequest, Verifier, verify_request};
21use axum::body::Body;
22use axum::http::{Request, Response};
23use tower::{Layer, Service};
24
25#[cfg(not(target_arch = "wasm32"))]
26trait MaybeSend: Send {}
27
28#[cfg(not(target_arch = "wasm32"))]
29impl<T: Send + ?Sized> MaybeSend for T {}
30
31#[cfg(target_arch = "wasm32")]
32trait MaybeSend {}
33
34#[cfg(target_arch = "wasm32")]
35impl<T: ?Sized> MaybeSend for T {}
36
37#[cfg(not(target_arch = "wasm32"))]
38type BoxServiceFuture<E> = Pin<Box<dyn Future<Output = Result<Response<Body>, E>> + Send>>;
39
40#[cfg(target_arch = "wasm32")]
41type BoxServiceFuture<E> = Pin<Box<dyn Future<Output = Result<Response<Body>, E>>>>;
42
43#[cfg(all(target_arch = "wasm32", feature = "worker"))]
44type ServiceFuture<E> = send_wrapper::SendWrapper<BoxServiceFuture<E>>;
45
46#[cfg(not(all(target_arch = "wasm32", feature = "worker")))]
47type ServiceFuture<E> = BoxServiceFuture<E>;
48
49#[cfg(all(target_arch = "wasm32", feature = "worker"))]
50fn service_future<E>(future: BoxServiceFuture<E>) -> ServiceFuture<E> {
51 send_wrapper::SendWrapper::new(future)
52}
53
54#[cfg(not(all(target_arch = "wasm32", feature = "worker")))]
55fn service_future<E>(future: BoxServiceFuture<E>) -> ServiceFuture<E> {
56 future
57}
58
59/// Tower layer that verifies Clerk session JWTs and inserts a
60/// [`VerificationOutcome`](crate::core::VerificationOutcome) into request extensions. Valid bearer tokens (or
61/// `__session` cookies) produce `VerificationOutcome::Valid(auth)`. The
62/// layer is **non-rejecting** for missing or invalid credentials: it records
63/// the outcome and lets the request continue so downstream code can decide
64/// how to handle anonymous requests.
65///
66/// # Restricting accepted tokens
67///
68/// With the default configuration, **any** JWT signed by your Clerk instance
69/// key verifies, including tokens minted from Clerk JWT templates for
70/// third-party integrations, which legitimately reach browsers. If your
71/// instance uses JWT templates, configure
72/// [`ClerkAuthLayerConfig::with_issuers`] and
73/// [`ClerkAuthLayerConfig::with_authorized_parties`] (and audiences where
74/// applicable) via [`ClerkAuthLayer::from_config`] so integration tokens
75/// cannot pass as session tokens.
76///
77/// # Verification model and limitations
78///
79/// Verification is **stateless**: each request is checked purely against the
80/// cached JWKS signing keys, with no call back to Clerk to consult live session
81/// state. Two consequences follow, both inherent to networkless JWT
82/// verification and matching Clerk's own backend model:
83///
84/// - **No revocation window.** A token whose session has since been signed out
85/// or revoked stays accepted until its `exp`. Clerk session tokens are
86/// short-lived (about a minute), so the exposure is bounded by that lifetime
87/// rather than by revocation. Gate anything that must react to revocation
88/// immediately on a fresh check rather than on a still-valid token.
89/// - **Key-rotation lag.** A token signed with a `kid` not in the cached JWKS
90/// triggers at most one refresh per unknown-kid refresh interval (5 minutes);
91/// within that window an unknown `kid` is rejected as invalid. Clerk
92/// pre-publishes new keys before signing with them, so this affects only
93/// rotations faster than the refresh floor, and the floor exists to keep an
94/// attacker from forcing unbounded JWKS refetches.
95///
96/// # `worker` feature
97///
98/// With the `worker` feature (server on wasm), the service future is wrapped
99/// in `SendWrapper` to satisfy Axum's `Send` bound. This assumes a
100/// single-threaded runtime such as Cloudflare Workers: the future must be
101/// polled and dropped on the thread that created it, and doing otherwise
102/// panics deterministically.
103#[derive(Clone)]
104pub struct ClerkAuthLayer {
105 inner: Arc<Inner>,
106}
107
108impl std::fmt::Debug for ClerkAuthLayer {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct("ClerkAuthLayer").finish_non_exhaustive()
111 }
112}
113
114struct Inner {
115 verifier: Verifier,
116}
117
118impl ClerkAuthLayer {
119 /// Build a layer that verifies tokens against Clerk's live JWKS using
120 /// the given backend secret key. The JWKS is fetched lazily on the
121 /// first valid-looking request and cached in memory.
122 ///
123 /// Uses the default configuration; see the type-level docs on
124 /// [restricting accepted tokens](ClerkAuthLayer#restricting-accepted-tokens)
125 /// when the Clerk instance mints JWT-template tokens.
126 pub fn new(secret_key: impl Into<String>) -> Result<Self, crate::core::ClerkError> {
127 Self::from_config(ClerkAuthLayerConfig::new(secret_key))
128 }
129
130 /// Build a layer from the conventional `CLERK_SECRET_KEY` environment variable.
131 ///
132 /// Uses the default configuration; see the type-level docs on
133 /// [restricting accepted tokens](ClerkAuthLayer#restricting-accepted-tokens)
134 /// when the Clerk instance mints JWT-template tokens.
135 pub fn from_env() -> Result<Self, crate::core::ClerkError> {
136 Self::from_config(ClerkAuthLayerConfig::from_env()?)
137 }
138
139 /// Build a layer from owned verifier configuration.
140 ///
141 /// Use this when an application needs to override Clerk backend settings or
142 /// enable optional claim validation.
143 pub fn from_config(config: ClerkAuthLayerConfig) -> Result<Self, crate::core::ClerkError> {
144 let verifier = Verifier::new(config)?;
145 Ok(Self {
146 inner: Arc::new(Inner { verifier }),
147 })
148 }
149}
150
151impl<S> Layer<S> for ClerkAuthLayer {
152 type Service = ClerkAuthService<S>;
153
154 fn layer(&self, inner: S) -> Self::Service {
155 ClerkAuthService {
156 inner,
157 layer: self.clone(),
158 }
159 }
160}
161
162/// Tower service produced by [`ClerkAuthLayer`].
163#[derive(Clone)]
164pub struct ClerkAuthService<S> {
165 inner: S,
166 layer: ClerkAuthLayer,
167}
168
169impl<S: std::fmt::Debug> std::fmt::Debug for ClerkAuthService<S> {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.debug_struct("ClerkAuthService")
172 .field("inner", &self.inner)
173 .finish_non_exhaustive()
174 }
175}
176
177impl<S> Service<Request<Body>> for ClerkAuthService<S>
178where
179 S: Service<Request<Body>, Response = Response<Body>> + Clone + MaybeSend + 'static,
180 S::Future: MaybeSend + 'static,
181{
182 type Response = Response<Body>;
183 type Error = S::Error;
184 type Future = ServiceFuture<S::Error>;
185
186 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
187 self.inner.poll_ready(cx)
188 }
189
190 fn call(&mut self, req: Request<Body>) -> Self::Future {
191 let layer = self.layer.clone();
192 let clone = self.inner.clone();
193 let mut inner = std::mem::replace(&mut self.inner, clone);
194
195 service_future(Box::pin(async move {
196 match verify_request(req, layer.inner.verifier.clone()).await {
197 VerifiedRequest::Forward(req) => inner.call(req).await,
198 VerifiedRequest::Unavailable(response) => Ok(response),
199 }
200 }))
201 }
202}