Skip to main content

dioxus_clerk/server/
config.rs

1//! Configuration for server-side Clerk request verification.
2
3use std::time::Duration;
4
5use crate::core::ClerkError;
6
7const DEFAULT_BACKEND_API_BASE_URL: &str = "https://api.clerk.com/v1";
8pub(crate) const DEFAULT_JWKS_CACHE_TTL: Duration = Duration::from_secs(60 * 60);
9pub(crate) const DEFAULT_UNKNOWN_KID_REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 5);
10/// After a failed JWKS fetch, further refresh attempts are suppressed for this
11/// long so an upstream outage does not serialize every request into its own
12/// slow fetch while holding the refresh lock.
13pub(crate) const DEFAULT_JWKS_FAILURE_BACKOFF: Duration = Duration::from_secs(10);
14/// How long past the cache TTL a stored keyset may still be served when a
15/// refresh fails (stale-while-error), so a Clerk outage does not turn every
16/// credentialed request into a 503 while the cached keys would still verify.
17pub(crate) const DEFAULT_JWKS_MAX_STALE: Duration = Duration::from_secs(60 * 60 * 24);
18pub(crate) const DEFAULT_CLOCK_SKEW: Duration = Duration::from_secs(5);
19
20/// Configuration for [`crate::server::ClerkAuthLayer`].
21///
22/// # Security: default claim acceptance
23///
24/// By default (with no issuers, audiences, or authorized parties configured)
25/// verification accepts **any** RS256 JWT that is signed by your instance's
26/// JWKS and passes standard `exp`/`nbf` validation. The `iss`, `aud`, and
27/// `azp` claims are not checked.
28///
29/// This is safe for the common single-instance case: the JWKS endpoint is
30/// scoped to your Clerk instance by the secret key, so a token that verifies
31/// against it was necessarily minted by *your* instance. It is not a bypass.
32///
33/// # Security: session tokens only, by default
34///
35/// Verification accepts only Clerk **session** tokens by default: a token must
36/// carry a `sid` (session id) claim. Clerk **JWT-template** tokens (minted with
37/// `getToken({ template })` for third-party integrations) are signed by the
38/// same instance JWKS and carry a `sub`, but omit `sid`, so without this check
39/// a leaked template token could be replayed to authenticate as its user.
40/// Requiring `sid` rejects them while accepting every genuine session token.
41/// Opt out with [`allow_non_session_tokens`](Self::allow_non_session_tokens)
42/// only if you deliberately verify non-session Clerk JWTs here.
43///
44/// The claim checks below are further defense-in-depth for setups where the
45/// single-instance assumption is weaker: multiple apps or environments sharing
46/// an instance, satellite domains, or tokens minted for a different audience
47/// that you do not want one service to accept for another. For those, harden
48/// verification:
49///
50/// - [`add_authorized_party`](Self::add_authorized_party): restrict the
51///   `azp` origins that may present tokens (Clerk's recommended check; tokens
52///   without `azp` are still accepted, matching Clerk's guidance).
53/// - [`add_issuer`](Self::add_issuer): pin the instance frontend origin.
54/// - [`add_audience`](Self::add_audience): require a specific `aud` when you
55///   mint audience-scoped tokens.
56#[derive(Clone)]
57pub struct ClerkAuthLayerConfig {
58    pub(crate) secret_key: String,
59    backend_api_base_url: String,
60    allow_insecure_backend_api_base_url: bool,
61    pub(crate) static_jwks: Option<String>,
62    pub(crate) authorized_parties: Vec<String>,
63    pub(crate) audiences: Vec<String>,
64    pub(crate) issuers: Vec<String>,
65    pub(crate) clock_skew: Duration,
66    pub(crate) require_session_id: bool,
67}
68
69impl ClerkAuthLayerConfig {
70    /// Creates a config using Clerk's default Backend API base URL.
71    pub fn new(secret_key: impl Into<String>) -> Self {
72        Self {
73            secret_key: secret_key.into(),
74            backend_api_base_url: DEFAULT_BACKEND_API_BASE_URL.into(),
75            allow_insecure_backend_api_base_url: false,
76            static_jwks: None,
77            authorized_parties: vec![],
78            audiences: vec![],
79            issuers: vec![],
80            clock_skew: DEFAULT_CLOCK_SKEW,
81            require_session_id: true,
82        }
83    }
84
85    /// Creates a config from the conventional `CLERK_SECRET_KEY` environment variable.
86    pub fn from_env() -> Result<Self, ClerkError> {
87        let secret_key = std::env::var("CLERK_SECRET_KEY").map_err(|_| {
88            ClerkError::InvalidConfig("missing CLERK_SECRET_KEY environment variable".into())
89        })?;
90        Ok(Self::new(secret_key))
91    }
92
93    /// Sets the HTTPS Clerk Backend API base URL, including API version.
94    ///
95    /// For example: `https://api.clerk.com/v1`. The JWKS endpoint is derived
96    /// by appending `/jwks` to this base URL.
97    pub fn with_backend_api_base_url(mut self, url: impl Into<String>) -> Self {
98        self.backend_api_base_url = url.into();
99        self.allow_insecure_backend_api_base_url = false;
100        self
101    }
102
103    /// Sets a local/test-only HTTP Backend API base URL.
104    ///
105    /// This explicitly allows `http://` URLs for local JWKS mocks. Do not use
106    /// it in production; plain HTTP allows JWKS response tampering.
107    pub fn with_insecure_backend_api_base_url(mut self, url: impl Into<String>) -> Self {
108        let url = url.into();
109        tracing::warn!(
110            url = %url,
111            "Clerk backend API base URL configured over insecure HTTP: JWKS signing keys can be tampered with in transit. Use with_backend_api_base_url with an https:// URL outside of local development and tests."
112        );
113        self.backend_api_base_url = url;
114        self.allow_insecure_backend_api_base_url = true;
115        self
116    }
117
118    /// Verifies against a fixed JWKS instead of fetching one from Clerk.
119    ///
120    /// `jwks_json` is a JWKS document (`{"keys": [...]}`). When set, the
121    /// Backend API base URL is never contacted: there is no HTTP client, no
122    /// cache, and no refresh, so the secret key is unused and may be empty.
123    ///
124    /// This is the offline path for tests — pair it with
125    /// [`TestIssuer`](crate::testing::TestIssuer) to verify locally minted
126    /// tokens without standing up a JWKS mock server at all.
127    ///
128    /// It is also usable in production to pin signing keys, but note that a
129    /// fixed keyset does not rotate: when Clerk rotates its signing keys,
130    /// every token signed by a new key is rejected until the configured JWKS
131    /// is updated and the process restarts. Prefer the fetched default unless
132    /// you have a specific reason to pin.
133    pub fn with_static_jwks(mut self, jwks_json: impl Into<String>) -> Self {
134        self.static_jwks = Some(jwks_json.into());
135        self
136    }
137
138    /// Adds a single allowed `azp` authorized-party origin to the configured set.
139    pub fn add_authorized_party(mut self, party: impl Into<String>) -> Self {
140        self.authorized_parties.push(party.into());
141        self
142    }
143
144    /// Sets allowed `azp` authorized-party origins.
145    ///
146    /// When configured, a token with an `azp` claim must match one of these
147    /// values. Tokens without `azp` continue to be accepted, matching Clerk's
148    /// manual verification guidance.
149    pub fn with_authorized_parties(
150        mut self,
151        parties: impl IntoIterator<Item = impl Into<String>>,
152    ) -> Self {
153        self.authorized_parties = parties.into_iter().map(Into::into).collect();
154        self
155    }
156
157    /// Adds a single allowed JWT audience to the configured set.
158    pub fn add_audience(mut self, audience: impl Into<String>) -> Self {
159        self.audiences.push(audience.into());
160        self
161    }
162
163    /// Sets allowed JWT audiences.
164    pub fn with_audiences(
165        mut self,
166        audiences: impl IntoIterator<Item = impl Into<String>>,
167    ) -> Self {
168        self.audiences = audiences.into_iter().map(Into::into).collect();
169        self
170    }
171
172    /// Adds a single allowed JWT issuer to the configured set.
173    ///
174    /// Clerk session tokens carry the instance frontend origin as `iss`
175    /// (for example `https://your-app.clerk.accounts.dev`). When configured,
176    /// tokens whose `iss` claim is missing or does not match are rejected.
177    pub fn add_issuer(mut self, issuer: impl Into<String>) -> Self {
178        self.issuers.push(issuer.into());
179        self
180    }
181
182    /// Sets allowed JWT issuers.
183    pub fn with_issuers(mut self, issuers: impl IntoIterator<Item = impl Into<String>>) -> Self {
184        self.issuers = issuers.into_iter().map(Into::into).collect();
185        self
186    }
187
188    /// Sets accepted clock skew for JWT `exp`, `nbf`, and `iat` validation.
189    pub fn with_clock_skew(mut self, clock_skew: Duration) -> Self {
190        self.clock_skew = clock_skew;
191        self
192    }
193
194    /// Accept instance-signed JWTs that are not session tokens (tokens without
195    /// a `sid` claim, such as Clerk JWT-template tokens).
196    ///
197    /// By default verification requires a `sid` so a leaked JWT-template token
198    /// cannot be replayed as a session (see the type-level security note). Call
199    /// this only when you deliberately verify non-session Clerk JWTs here, and
200    /// pair it with [`add_audience`](Self::add_audience) to keep tokens from one
201    /// template being accepted for another.
202    pub fn allow_non_session_tokens(mut self) -> Self {
203        self.require_session_id = false;
204        self
205    }
206
207    pub(crate) fn jwks_url(&self) -> Result<reqwest::Url, ClerkError> {
208        let jwks_url = format!("{}/jwks", self.backend_api_base_url.trim_end_matches('/'));
209        let url = reqwest::Url::parse(&jwks_url).map_err(|error| {
210            ClerkError::InvalidConfig(format!("invalid Clerk Backend API base URL: {error}"))
211        })?;
212
213        if url.scheme() != "https" && !self.allow_insecure_backend_api_base_url {
214            return Err(ClerkError::InvalidConfig(
215                "Clerk Backend API base URL must use https; use with_insecure_backend_api_base_url only for local tests".into(),
216            ));
217        }
218
219        Ok(url)
220    }
221}
222
223impl std::fmt::Debug for ClerkAuthLayerConfig {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        f.debug_struct("ClerkAuthLayerConfig")
226            .field("secret_key", &"<redacted>")
227            .field("backend_api_base_url", &self.backend_api_base_url)
228            .field(
229                "allow_insecure_backend_api_base_url",
230                &self.allow_insecure_backend_api_base_url,
231            )
232            .field("static_jwks", &self.static_jwks.is_some())
233            .field("authorized_parties", &self.authorized_parties)
234            .field("audiences", &self.audiences)
235            .field("issuers", &self.issuers)
236            .field("clock_skew", &self.clock_skew)
237            .field("require_session_id", &self.require_session_id)
238            .finish()
239    }
240}