cf_access/lib.rs
1/*
2 * MIT License
3 *
4 * Copyright (c) 2025 Jasmine Tai
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy of
7 * this software and associated documentation files (the "Software"), to deal in
8 * the Software without restriction, including without limitation the rights to
9 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
10 * of the Software, and to permit persons to whom the Software is furnished to do
11 * so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in all
14 * copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24
25#![doc = include_str!("../README.md")]
26
27mod claims;
28mod error;
29
30use std::time::Duration;
31
32use jwtk::jwk::RemoteJwksVerifier;
33use reqwest::Client;
34
35pub use claims::*;
36pub use error::Error;
37pub use jwtk;
38pub use reqwest;
39pub use uuid;
40
41/// A validator for Cloudflare Access JWTs.
42pub struct Validator {
43 inner: RemoteJwksVerifier,
44 audience: String,
45}
46
47impl Validator {
48 /// Creates a new [`Validator`] with the given Cloudflare Access team name
49 /// and application AUD tag.
50 pub fn new(team_name: &str, audience: impl Into<String>) -> Self {
51 Validator::with_client(Client::default(), team_name, audience)
52 }
53
54 /// Creates a new [`Validator`] from the current process's environment
55 /// variables.
56 #[cfg(feature = "env")]
57 pub fn from_env() -> Result<Self, Error> {
58 fn var(name: &'static str) -> Result<String, Error> {
59 std::env::var(name).map_err(|_| Error::MissingEnv(name))
60 }
61
62 let team_name = var("CF_ACCESS_TEAM")?;
63 let audience = var("CF_ACCESS_AUD")?;
64 Ok(Validator::new(&team_name, audience))
65 }
66
67 /// Creates a new [`Validator`] that uses a specific [`reqwest::Client`].
68 pub fn with_client(client: Client, team_name: &str, audience: impl Into<String>) -> Self {
69 let issuer = format!("https://{team_name}.cloudflareaccess.com");
70 let url = format!("{issuer}/cdn-cgi/access/certs");
71 Validator {
72 inner: RemoteJwksVerifier::new(url, Some(client), CACHE_DURATION),
73 audience: audience.into(),
74 }
75 }
76
77 /// Validates the JWT.
78 pub async fn validate(&self, jwt: &str) -> Result<Claims, Error> {
79 let mut token = self.inner.verify::<ClaimsExtra>(jwt).await?;
80 if !token.claims().aud.iter().any(|aud| **aud == self.audience) {
81 return Err(Error::InvalidAud);
82 }
83 let claims = std::mem::take(token.claims_mut());
84
85 let token = match claims.extra {
86 ClaimsExtra::Identity {
87 email,
88 ty,
89 identity_nonce,
90 country,
91 } => IdentityClaims {
92 sub: claims.sub.ok_or(Error::MissingSub)?.parse()?,
93 email,
94 ty,
95 identity_nonce,
96 country,
97 }
98 .into(),
99 ClaimsExtra::Service(claims) => claims.into(),
100 };
101 Ok(token)
102 }
103}
104
105const CACHE_DURATION: Duration = Duration::from_secs(60 * 60 * 24 * 3); // 3 days