Skip to main content

alibi_api/plugins/jwt/
keyring.rs

1use super::*;
2impl JwtPlugin {
3    pub(in crate::plugins::jwt) async fn keys(
4        &self,
5        request: Option<&AuthRequest>,
6        ctx: &AuthContext<impl AuthSchema>,
7    ) -> AuthResult<Vec<Jwk>> {
8        let endpoint = alibi_core::endpoint::current_endpoint_call_context();
9        let path = endpoint
10            .as_ref()
11            .and_then(alibi_core::endpoint::EndpointCall::path)
12            .unwrap_or_else(|| request.map_or("virtual:", AuthRequest::path));
13        self.keys_at_path(request, path, ctx).await
14    }
15
16    pub(in crate::plugins::jwt) async fn keys_in_transaction<S: AuthSchema>(
17        &self,
18        request: Option<&AuthRequest>,
19        ctx: &AuthContext<S>,
20        transaction: Option<&dyn alibi_core::store::AuthTransaction<S>>,
21    ) -> AuthResult<Vec<Jwk>> {
22        if self.config.keyring.is_none()
23            && let Some(transaction) = transaction
24        {
25            transaction.list_jwks().await
26        } else {
27            self.keys(request, ctx).await
28        }
29    }
30
31    pub(in crate::plugins::jwt) async fn keys_at_path(
32        &self,
33        request: Option<&AuthRequest>,
34        path: &str,
35        ctx: &AuthContext<impl AuthSchema>,
36    ) -> AuthResult<Vec<Jwk>> {
37        let endpoint = alibi_core::endpoint::current_endpoint_call_context();
38        match &self.config.keyring {
39            Some(keyring) => {
40                keyring
41                    .keys(&JwtKeyringContext {
42                        path,
43                        request,
44                        endpoint: endpoint.as_ref(),
45                    })
46                    .await
47            }
48            None => ctx.database.list_jwks().await,
49        }
50    }
51
52    /// Provision a private signing key and its public JWK in persistent storage.
53    ///
54    /// # Errors
55    ///
56    /// Returns an error if key generation, key serialization, or JWK storage fails.
57    pub async fn create_jwk<S: AuthSchema>(
58        &self,
59        config: Option<&JwtKeyPairConfig>,
60        request: Option<&AuthRequest>,
61        ctx: &AuthContext<S>,
62    ) -> AuthResult<Jwk> {
63        self.create_jwk_in_transaction(config, request, ctx, None)
64            .await
65    }
66
67    pub(in crate::plugins::jwt) async fn create_jwk_in_transaction<S: AuthSchema>(
68        &self,
69        config: Option<&JwtKeyPairConfig>,
70        request: Option<&AuthRequest>,
71        ctx: &AuthContext<S>,
72        transaction: Option<&dyn alibi_core::store::AuthTransaction<S>>,
73    ) -> AuthResult<Jwk> {
74        let config = config.unwrap_or(&self.config.key_pair);
75        let (public, private) = crypto::generate(config)?;
76        let private = serde_json::to_string(&private)?;
77        let private_key = if self.config.disable_private_key_encryption {
78            private
79        } else {
80            serde_json::to_string(&encrypt_with_config(&private, &ctx.config)?)?
81        };
82        let now = Utc::now();
83        let data = CreateJwk {
84            id: None,
85            public_key: serde_json::to_string(&public)?,
86            private_key,
87            created_at: now,
88            expires_at: self
89                .config
90                .rotation_interval
91                .filter(|duration| !duration.is_zero())
92                .map(|duration| now + duration),
93            alg: Some(config.algorithm.as_str().to_owned()),
94            crv: config.algorithm.curve().map(str::to_owned),
95        };
96        let endpoint = alibi_core::endpoint::current_endpoint_call_context();
97        match &self.config.keyring {
98            Some(keyring) => {
99                keyring
100                    .create_key(
101                        data,
102                        &JwtKeyringContext {
103                            path: endpoint
104                                .as_ref()
105                                .and_then(alibi_core::endpoint::EndpointCall::path)
106                                .unwrap_or_else(|| request.map_or("virtual:", AuthRequest::path)),
107                            request,
108                            endpoint: endpoint.as_ref(),
109                        },
110                    )
111                    .await
112            }
113            None => match transaction {
114                Some(transaction) => transaction.create_jwk(data).await,
115                None => ctx.database.create_jwk(data).await,
116            },
117        }
118    }
119
120    /// Select a live key, with explicit key or algorithm pinning when requested.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if a usable signing key cannot be loaded or generated.
125    pub async fn resolve_signing_key<S: AuthSchema>(
126        &self,
127        options: &JwtSignOptions,
128        request: Option<&AuthRequest>,
129        ctx: &AuthContext<S>,
130    ) -> AuthResult<Option<ResolvedJwtSigningKey>> {
131        self.resolve_signing_key_in_transaction(options, request, ctx, None)
132            .await
133    }
134
135    pub(in crate::plugins::jwt) async fn resolve_signing_key_in_transaction<S: AuthSchema>(
136        &self,
137        options: &JwtSignOptions,
138        request: Option<&AuthRequest>,
139        ctx: &AuthContext<S>,
140        transaction: Option<&dyn alibi_core::store::AuthTransaction<S>>,
141    ) -> AuthResult<Option<ResolvedJwtSigningKey>> {
142        if self.config.remote_signer.is_some() {
143            return Ok(None);
144        }
145        // Pinned IDs use findOne independently of the adapter's findMany
146        // limit. Custom keyrings supply the raw full set once for this lookup.
147        let mut keys = if options.signing_key_id.is_some() {
148            Vec::new()
149        } else {
150            self.keys_in_transaction(request, ctx, transaction).await?
151        };
152        keys.sort_by_key(|key| std::cmp::Reverse(key.created_at));
153        let primary = self.config.key_pair.algorithm;
154        let key_alg = |key: &Jwk| {
155            key.alg
156                .as_deref()
157                .map_or(Ok(primary), JwtAlgorithm::from_str)
158        };
159        let now = Utc::now();
160        let live = |key: &&Jwk| key.expires_at.is_none_or(|expiry| expiry > now);
161        let mut minted_unpinned_key = false;
162        let mut key = if let Some(id) = &options.signing_key_id {
163            let key = match &self.config.keyring {
164                Some(_) => self.keys_in_transaction(request, ctx, transaction).await?.into_iter().find(|key| &key.id == id),
165                None => match transaction {Some(transaction)=>transaction.get_jwk_by_id(id).await?,None=>ctx.database.get_jwk_by_id(id).await?},
166            }.ok_or_else(|| AuthError::config(format!("signJWT: signingKeyId \"{id}\" not found in JWKS. The key must be provisioned before it can be referenced.")))?;
167            if let Some(algorithm) = options.signing_algorithm
168                && key_alg(&key)? != algorithm
169            {
170                return Err(AuthError::config(format!(
171                    "signJWT: signingKeyId \"{id}\" has a different algorithm than {}",
172                    algorithm.as_str()
173                )));
174            }
175            key
176        } else if let Some(algorithm) = options.signing_algorithm {
177            if let Some(key) = keys
178                .iter()
179                .filter(live)
180                .find(|key| key_alg(key).is_ok_and(|alg| alg == algorithm))
181                .cloned()
182            {
183                key
184            } else {
185                let config = self
186                    .config
187                    .additional_key_pairs
188                    .iter()
189                    .find(|config| config.algorithm == algorithm)
190                    .or_else(|| (primary == algorithm).then_some(&self.config.key_pair))
191                    .ok_or_else(|| {
192                        AuthError::config(format!(
193                            "No signing key configured for {}",
194                            algorithm.as_str()
195                        ))
196                    })?;
197                self.create_jwk_in_transaction(Some(config), request, ctx, transaction)
198                    .await?
199            }
200        } else {
201            if let Some(key) = keys
202                .iter()
203                .filter(live)
204                .find(|key| key_alg(key).is_ok_and(|alg| alg == primary))
205                .cloned()
206            {
207                key
208            } else {
209                // Source performs a separate fallback lookup. An application
210                // keyring can observe it or return a changed key set.
211                let mut fallback = self.keys_in_transaction(request, ctx, transaction).await?;
212                fallback.sort_by_key(|key| std::cmp::Reverse(key.created_at));
213                if let Some(key) = fallback
214                    .into_iter()
215                    .find(|key| key.expires_at.is_none_or(|expiry| expiry > Utc::now()))
216                {
217                    key
218                } else {
219                    minted_unpinned_key = true;
220                    self.create_jwk_in_transaction(None, request, ctx, transaction)
221                        .await?
222                }
223            }
224        };
225        if !minted_unpinned_key && key.expires_at.is_some_and(|expiry| expiry < Utc::now()) {
226            if options.signing_key_id.is_some() || options.signing_algorithm.is_some() {
227                return Err(AuthError::config(
228                    "signJWT: requested signing key is expired and an explicit kid/alg was provided; not auto-minting a replacement. Rotate the key explicitly.",
229                ));
230            }
231            key = self
232                .create_jwk_in_transaction(None, request, ctx, transaction)
233                .await?;
234        }
235        let private = if self.config.disable_private_key_encryption {
236            key.private_key
237        } else {
238            let encrypted: String = serde_json::from_str(&key.private_key)?;
239            decrypt_with_config(&encrypted, &ctx.config).map_err(|_error| AuthError::config("Failed to decrypt private key. Make sure the secret currently in use is the same as the one used to encrypt the private key. If you are using a different secret, either clean up your JWKS or disable private key encryption."))?
240        };
241        Ok(Some(ResolvedJwtSigningKey {
242            algorithm: key
243                .alg
244                .as_deref()
245                .map(JwtAlgorithm::from_str)
246                .unwrap_or(Ok(primary))?,
247            key_id: key.id,
248            private_key: serde_json::from_str(&private)?,
249        }))
250    }
251}