Skip to main content

openstack_keystone_core/token/backend/
fernet.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14
15use base64::Engine;
16use byteorder::ReadBytesExt;
17use bytes::Bytes;
18use chrono::{DateTime, Utc};
19use fernet::MultiFernet;
20use itertools::Itertools;
21use rmp::{
22    Marker,
23    decode::{ValueReadError, read_marker, read_u8},
24    encode::{write_array_len, write_pfix},
25};
26use std::collections::BTreeMap;
27use std::collections::HashSet;
28use std::fmt;
29use std::io::{Cursor, Write};
30use tracing::trace;
31use validator::Validate;
32
33use crate::config::Config;
34use crate::token::backend::TokenBackend;
35use crate::token::{
36    TokenProviderError,
37    types::{
38        application_credential::ApplicationCredentialPayload, domain_scoped::DomainScopePayload,
39        federation_domain_scoped::FederationDomainScopePayload,
40        federation_project_scoped::FederationProjectScopePayload,
41        federation_unscoped::FederationUnscopedPayload, project_scoped::ProjectScopePayload,
42        restricted::RestrictedPayload, trust::TrustPayload, unscoped::UnscopedPayload, *,
43    },
44};
45use utils::FernetUtils;
46
47mod application_credential;
48mod domain_scoped;
49mod federation_domain_scoped;
50mod federation_project_scoped;
51mod federation_unscoped;
52mod project_scoped;
53mod restricted;
54mod system_scoped;
55mod trust;
56mod unscoped;
57pub mod utils;
58
59/// Fernet token provider.
60#[derive(Clone)]
61pub struct FernetTokenProvider {
62    config: Config,
63    utils: FernetUtils,
64    fernet: Option<MultiFernet>,
65    /// Map of the configured authentication methods.
66    auth_map: BTreeMap<u8, String>,
67    /// Cached permutations of auth_methods to the payload code.
68    auth_methods_code_cache: BTreeMap<u8, HashSet<String>>,
69}
70
71pub trait MsgPackToken {
72    type Token;
73
74    /// Construct MsgPack payload for the Token.
75    fn assemble<W: Write>(
76        &self,
77        _wd: &mut W,
78        _fernet_provider: &FernetTokenProvider,
79    ) -> Result<(), TokenProviderError> {
80        Ok(())
81    }
82
83    /// Parse MsgPack payload into the Token.
84    fn disassemble(
85        rd: &mut &[u8],
86        fernet_provider: &FernetTokenProvider,
87    ) -> Result<Self::Token, TokenProviderError>;
88}
89
90impl fmt::Debug for FernetTokenProvider {
91    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
92        f.debug_struct("FernetTokenProvider").finish()
93    }
94}
95
96/// Read the payload version.
97fn read_payload_token_type(rd: &mut &[u8]) -> Result<u8, TokenProviderError> {
98    match read_marker(rd).map_err(ValueReadError::from)? {
99        Marker::FixPos(dt) => Ok(dt),
100        Marker::U8 => Ok(read_u8(rd)?),
101        _ => Err(TokenProviderError::InvalidToken),
102    }
103}
104
105/// Calculate possible combinations of the vector string elements.
106fn all_combinations<I>(iter: I) -> impl IntoIterator<Item = HashSet<String>>
107where
108    I: IntoIterator<Item = String>,
109{
110    let items: Vec<String> = iter.into_iter().collect();
111    let n = items.len();
112    let mut result = Vec::new();
113
114    // There are 2^n possible subsets
115    for mask in 0..(1 << n) {
116        let mut subset = HashSet::new();
117        for (i, am) in items.iter().enumerate() {
118            if (mask & (1 << i)) != 0 {
119                subset.insert(am.clone());
120            }
121        }
122        result.push(subset);
123    }
124    result.into_iter().filter(|v| !v.is_empty())
125}
126
127impl FernetTokenProvider {
128    /// Construct new FernetTokenProvider.
129    pub fn new(config: Config) -> Self {
130        let mut slf = Self {
131            utils: FernetUtils {
132                key_repository: config.fernet_tokens.key_repository.clone(),
133                max_active_keys: config.fernet_tokens.max_active_keys,
134            },
135            config,
136            fernet: None,
137            auth_map: BTreeMap::new(),
138            auth_methods_code_cache: BTreeMap::new(),
139        };
140        slf.reload_config();
141        slf
142    }
143
144    pub fn reload_config(&mut self) {
145        self.auth_map = BTreeMap::from_iter(
146            self.config
147                .auth
148                .methods
149                .iter()
150                .enumerate()
151                .map(|(k, v)| (1 << k, v.clone())),
152        );
153        self.set_auth_methods_cache_combinations();
154    }
155
156    fn set_auth_methods_cache_combinations(&mut self) {
157        self.auth_methods_code_cache.clear();
158        for auth_pairs in all_combinations(self.auth_map.values().cloned()) {
159            let pair: HashSet<String> = HashSet::from_iter(auth_pairs.into_iter());
160            self.encode_auth_methods(pair.clone())
161                .ok()
162                .map(|val| self.auth_methods_code_cache.insert(val, pair));
163        }
164    }
165
166    /// Encode the list of auth_methods into a single integer
167    #[tracing::instrument(level = "trace", skip(self, methods))]
168    pub(crate) fn encode_auth_methods<I>(&self, methods: I) -> Result<u8, TokenProviderError>
169    where
170        I: IntoIterator<Item = String>,
171    {
172        let me: HashSet<String> = HashSet::from_iter(methods.into_iter());
173        let res = self
174            .auth_map
175            .iter()
176            .fold(0, |acc, (k, v)| acc + if me.contains(v) { *k } else { 0 });
177
178        // TODO: Improve unit tests to ensure unsupported auth method immediately raises
179        // error.
180        if res == 0 {
181            return Err(TokenProviderError::UnsupportedAuthMethods(
182                me.iter().join(","),
183            ));
184        }
185        Ok(res)
186    }
187
188    /// Decode the integer into the list of auth_methods
189    #[tracing::instrument(level = "trace", skip(self))]
190    pub(crate) fn decode_auth_methods(&self, value: u8) -> Result<Vec<String>, TokenProviderError> {
191        if let Some(res) = self.auth_methods_code_cache.get(&value) {
192            Ok(res.iter().cloned().collect())
193        } else {
194            trace!("Auth methods cache miss.");
195            let mut results: Vec<String> = Vec::new();
196            let mut auth: u8 = value;
197            for (idx, name) in self.auth_map.iter() {
198                // (lbragstad): By dividing the method_int by each key in the
199                // method_map, we know if the division results in an integer of 1, that
200                // key was used in the construction of the total sum of the method_int.
201                // In that case, we should confirm the key value and store it so we can
202                // look it up later. Then we should take the remainder of what is
203                // confirmed and the method_int and continue the process. In the end, we
204                // should have a list of integers that correspond to indexes in our
205                // method_map and we can reinflate the methods that the original
206                // method_int represents.
207                let result: u8 = auth / idx;
208                if result == 1 {
209                    results.push(name.clone());
210                    auth -= idx;
211                }
212            }
213            Ok(results)
214        }
215    }
216
217    /// Parse binary blob as MessagePack after encrypting it with Fernet.
218    fn decode(
219        &self,
220        rd: &mut &[u8],
221        timestamp: DateTime<Utc>,
222    ) -> Result<Token, TokenProviderError> {
223        if let Marker::FixArray(_) = read_marker(rd).map_err(ValueReadError::from)? {
224            let mut token: Token = match read_payload_token_type(rd)? {
225                0 => Ok(UnscopedPayload::disassemble(rd, self)?.into()),
226                1 => Ok(DomainScopePayload::disassemble(rd, self)?.into()),
227                2 => Ok(ProjectScopePayload::disassemble(rd, self)?.into()),
228                3 => Ok(TrustPayload::disassemble(rd, self)?.into()),
229                4 => Ok(FederationUnscopedPayload::disassemble(rd, self)?.into()),
230                5 => Ok(FederationProjectScopePayload::disassemble(rd, self)?.into()),
231                6 => Ok(FederationDomainScopePayload::disassemble(rd, self)?.into()),
232                8 => Ok(SystemScopePayload::disassemble(rd, self)?.into()),
233                9 => Ok(ApplicationCredentialPayload::disassemble(rd, self)?.into()),
234                11 => Ok(RestrictedPayload::disassemble(rd, self)?.into()),
235                other => Err(TokenProviderError::InvalidTokenType(other)),
236            }?;
237            token.set_issued_at(timestamp);
238            Ok(token.to_owned())
239        } else {
240            Err(TokenProviderError::InvalidToken)
241        }
242    }
243
244    /// Encode Token as binary blob as MessagePack.
245    fn encode(&self, token: &Token) -> Result<Bytes, TokenProviderError> {
246        token.validate()?;
247        let mut buf = vec![];
248        match token {
249            Token::ApplicationCredential(data) => {
250                write_array_len(&mut buf, 7)
251                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
252                write_pfix(&mut buf, 9)
253                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
254                data.assemble(&mut buf, self)?;
255            }
256            Token::DomainScope(data) => {
257                write_array_len(&mut buf, 6)
258                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
259                write_pfix(&mut buf, 1)
260                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
261                data.assemble(&mut buf, self)?;
262            }
263            Token::Trust(data) => {
264                write_array_len(&mut buf, 7)
265                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
266                write_pfix(&mut buf, 3)
267                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
268                data.assemble(&mut buf, self)?;
269            }
270            Token::FederationUnscoped(data) => {
271                write_array_len(&mut buf, 8)
272                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
273                write_pfix(&mut buf, 4)
274                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
275                data.assemble(&mut buf, self)?;
276            }
277            Token::FederationProjectScope(data) => {
278                write_array_len(&mut buf, 9)
279                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
280                write_pfix(&mut buf, 5)
281                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
282                data.assemble(&mut buf, self)?;
283            }
284            Token::FederationDomainScope(data) => {
285                write_array_len(&mut buf, 9)
286                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
287                write_pfix(&mut buf, 6)
288                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
289                data.assemble(&mut buf, self)?;
290            }
291            Token::ProjectScope(data) => {
292                write_array_len(&mut buf, 6)
293                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
294                write_pfix(&mut buf, 2)
295                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
296                data.assemble(&mut buf, self)?;
297            }
298            Token::Restricted(data) => {
299                write_array_len(&mut buf, 9)
300                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
301                write_pfix(&mut buf, 11)
302                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
303                data.assemble(&mut buf, self)?;
304            }
305            Token::SystemScope(data) => {
306                write_array_len(&mut buf, 6)
307                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
308                write_pfix(&mut buf, 8)
309                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
310                data.assemble(&mut buf, self)?;
311            }
312            Token::Unscoped(data) => {
313                write_array_len(&mut buf, 5)
314                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
315                write_pfix(&mut buf, 0)
316                    .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
317                data.assemble(&mut buf, self)?;
318            }
319        }
320        Ok(buf.into())
321    }
322
323    /// Get MultiFernet initialized with repository keys.
324    #[tracing::instrument(level = "trace", skip(self))]
325    pub fn get_fernet(&self) -> Result<MultiFernet, TokenProviderError> {
326        Ok(MultiFernet::new(
327            self.utils.load_keys()?.into_iter().collect::<Vec<_>>(),
328        ))
329    }
330
331    /// Load fernet keys from FS.
332    #[tracing::instrument(level = "trace", skip(self))]
333    pub fn load_keys(&mut self) -> Result<(), TokenProviderError> {
334        self.fernet = Some(self.get_fernet()?);
335        Ok(())
336    }
337
338    /// Decrypt the token.
339    ///
340    /// 1. Decrypt as Fernet.
341    /// 2. Unpack MessagePack payload.
342    pub fn decrypt(&self, credential: &str) -> Result<Token, TokenProviderError> {
343        // TODO: Implement fernet keys change watching. Keystone loads them from FS on
344        // every request and in the best case it costs 15µs.
345        let fernet = match &self.fernet {
346            Some(f) => f,
347            None => &self.get_fernet()?,
348        };
349        let payload = fernet.decrypt(credential)?;
350
351        self.decode(&mut payload.as_slice(), get_fernet_timestamp(credential)?)
352    }
353
354    /// Encrypt the token.
355    pub fn encrypt(&self, token: &Token) -> Result<String, TokenProviderError> {
356        let payload = self.encode(token)?;
357        let res = match &self.fernet {
358            Some(fernet) => fernet.encrypt(&payload),
359            _ => self.get_fernet()?.encrypt(&payload),
360        };
361        Ok(res)
362    }
363}
364
365impl TokenBackend for FernetTokenProvider {
366    /// Set config.
367    fn set_config(&mut self, config: Config) {
368        self.config = config;
369        self.reload_config();
370    }
371
372    /// Decrypt the token.
373    #[tracing::instrument(level = "trace", skip(self, credential))]
374    fn decode(&self, credential: &str) -> Result<Token, TokenProviderError> {
375        self.decrypt(credential)
376    }
377
378    /// Encrypt the token.
379    #[tracing::instrument(level = "trace", skip(self, token))]
380    fn encode(&self, token: &Token) -> Result<String, TokenProviderError> {
381        self.encrypt(token)
382    }
383}
384
385/// Decode the fernet payload as Base64_urlsafe.
386fn b64_decode_url(input: &str) -> std::result::Result<Vec<u8>, base64::DecodeError> {
387    base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(input.trim_end_matches('='))
388}
389
390/// Get the fernet payload creation timestamp.
391///
392/// Extract the payload creation timestamp in the UTC.
393fn get_fernet_timestamp(payload: &str) -> Result<DateTime<Utc>, TokenProviderError> {
394    let data = match b64_decode_url(payload) {
395        Ok(data) => data,
396        Err(_) => return Err(fernet::DecryptionError)?,
397    };
398
399    let mut input = Cursor::new(data);
400
401    match input.read_u8() {
402        Ok(0x80) => {}
403        _ => return Err(fernet::DecryptionError)?,
404    }
405
406    input
407        .read_u64::<byteorder::BigEndian>()
408        .map_err(|_| TokenProviderError::FernetDecryption(fernet::DecryptionError))
409        .and_then(|val| {
410            TryInto::try_into(val).map_err(|err| TokenProviderError::TokenTimestampOverflow {
411                value: val,
412                source: err,
413            })
414        })
415        .and_then(|val| {
416            DateTime::from_timestamp_secs(val)
417                .ok_or_else(|| TokenProviderError::FernetDecryption(fernet::DecryptionError))
418        })
419}
420
421// Conditionally expose the function when the 'bench_internals' feature is
422// enabled
423#[cfg(feature = "bench_internals")]
424pub fn bench_get_fernet_timestamp(payload: &str) -> Result<DateTime<Utc>, TokenProviderError> {
425    get_fernet_timestamp(payload)
426}
427
428#[cfg(test)]
429pub(super) mod tests {
430    use super::*;
431    use chrono::{Local, SubsecRound};
432    use std::fs::File;
433    use std::io::Write;
434    use tempfile::tempdir;
435    use uuid::Uuid;
436
437    pub(super) fn setup_config() -> Config {
438        let keys_dir = tempdir().unwrap();
439        // write fernet key used to generate tokens in python
440        let file_path = keys_dir.path().join("0");
441        let mut tmp_file = File::create(file_path).unwrap();
442        write!(tmp_file, "BFTs1CIVIBLTP4GOrQ26VETrJ7Zwz1O4wbEcCQ966eM=").unwrap();
443
444        let builder = config::Config::builder()
445            .set_override(
446                "auth.methods",
447                "password,token,openid,application_credential",
448            )
449            .unwrap()
450            .set_override("database.connection", "dummy")
451            .unwrap();
452        let mut config: Config = Config::try_from(builder).expect("can build a valid config");
453        config.fernet_tokens.key_repository = keys_dir.keep();
454        config
455    }
456
457    fn discard_issued_at(mut token: Token) -> Token {
458        token.set_issued_at(Default::default());
459        token
460    }
461
462    #[tokio::test]
463    async fn test_decrypt_unscoped() {
464        let token = "gAAAAABnt12vpnYCuUxl1lWQfTxwkBcZcgdK5wYons4BFHxxZLk326To5afinp29in7f5ZHR5K61Pl2voIjfbPKlL51KempshD4shfSje4RutbeXq-NT498eEcorzige5XBYGaoWuDTOKEDH2eXCMHhw9722j9iPP3Z4r_1Zlmcqq1n2tndmvsA";
465
466        let mut provider = FernetTokenProvider::new(setup_config());
467        provider.load_keys().unwrap();
468
469        if let Token::Unscoped(decrypted) = provider.decrypt(token).unwrap() {
470            assert_eq!(decrypted.user_id, "4b7d364ad87d400bbd91798e3c15e9c2");
471            let mut methods_curr = decrypted.methods.clone();
472            methods_curr.sort();
473            assert_eq!(methods_curr, ["password", "token"]);
474            assert_eq!(
475                decrypted.expires_at.to_rfc3339(),
476                "2025-02-20T17:40:13+00:00"
477            );
478            assert_eq!(
479                decrypted.audit_ids,
480                vec!["sfROvzgjTdmbo8xZdcze-g", "FL7FbzBKQsK115_4TyyiIw"]
481            );
482        } else {
483            panic!()
484        }
485    }
486
487    #[tokio::test]
488    async fn test_unscoped_roundtrip() {
489        let token = Token::Unscoped(UnscopedPayload {
490            user_id: Uuid::new_v4().simple().to_string(),
491            methods: vec!["password".into()],
492            audit_ids: vec!["Zm9vCg".into()],
493            expires_at: Local::now().trunc_subsecs(0).into(),
494            ..Default::default()
495        });
496
497        let mut provider = FernetTokenProvider::new(setup_config());
498        provider.load_keys().unwrap();
499
500        let encrypted = provider.encrypt(&token).unwrap();
501        let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap());
502        assert_eq!(token, dec_token);
503    }
504
505    #[tokio::test]
506    async fn test_decrypt_domain() {
507        let token = "gAAAAABnt16C_ve4dDc7TeU857pwTXGJfGqNA4uJ308_2o_F9T_8WenNBatll0Q36wGz79dSI6RQnuN2PbK17wxQbn9jXscDh2ie3ZrW-WL5gG3gWK6FiPleAiU3kJN5mkskViJOIN-ZpP2B15fmZiYijelQ9TQuhQ";
508
509        let mut provider = FernetTokenProvider::new(setup_config());
510        provider.load_keys().unwrap();
511
512        if let Token::DomainScope(decrypted) = provider.decrypt(token).unwrap() {
513            assert_eq!(decrypted.user_id, "4b7d364ad87d400bbd91798e3c15e9c2");
514            assert_eq!(decrypted.domain_id, "default");
515            assert_eq!(decrypted.methods, vec!["password"]);
516            assert_eq!(
517                decrypted.expires_at.to_rfc3339(),
518                "2025-02-20T17:55:30+00:00"
519            );
520            assert_eq!(decrypted.audit_ids, vec!["eikbCiM0SsO5P9d_GbVhBQ"]);
521        } else {
522            panic!()
523        }
524    }
525
526    #[tokio::test]
527    async fn test_domain_roundtrip() {
528        let token = Token::DomainScope(DomainScopePayload {
529            user_id: Uuid::new_v4().simple().to_string(),
530            methods: vec!["password".into()],
531            domain_id: Uuid::new_v4().simple().to_string(),
532            audit_ids: vec!["Zm9vCg".into()],
533            expires_at: Local::now().trunc_subsecs(0).into(),
534            ..Default::default()
535        });
536
537        let mut provider = FernetTokenProvider::new(setup_config());
538        provider.load_keys().unwrap();
539
540        let encrypted = provider.encrypt(&token).unwrap();
541        let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap());
542        assert_eq!(token, dec_token);
543    }
544
545    #[tokio::test]
546    async fn test_decrypt_project() {
547        let token = "gAAAAABns2ixy75K_KfoosWLrNNqG6KW8nm3Xzv0_2dOx8ODWH7B8i2g8CncGLO6XBEH_TYLg83P6XoKQ5bU8An8Kqgw9WX3bvmEQXphnwPM6aRAOQUSdVhTlUm_8otDG9BS2rc70Q7pfy57S3_yBgimy-174aKdP8LPusvdHZsQPEJO9pfeXWw";
548
549        let mut provider = FernetTokenProvider::new(setup_config());
550        provider.load_keys().unwrap();
551
552        if let Token::ProjectScope(decrypted) = provider.decrypt(token).unwrap() {
553            assert_eq!(decrypted.user_id, "4b7d364ad87d400bbd91798e3c15e9c2");
554            assert_eq!(decrypted.project_id, "97cd761d581b485792a4afc8cc6a998d");
555            assert_eq!(decrypted.methods, vec!["password"]);
556            assert_eq!(
557                decrypted.expires_at.to_rfc3339(),
558                "2025-02-17T17:49:53+00:00"
559            );
560            assert_eq!(decrypted.audit_ids, vec!["fhRNUHHPTkitISpEYkY_mQ"]);
561        } else {
562            panic!()
563        }
564    }
565
566    #[tokio::test]
567    async fn test_project_roundtrip() {
568        let token = Token::ProjectScope(ProjectScopePayload {
569            user_id: Uuid::new_v4().simple().to_string(),
570            methods: vec!["password".into()],
571            project_id: Uuid::new_v4().simple().to_string(),
572            audit_ids: vec!["Zm9vCg".into()],
573            expires_at: Local::now().trunc_subsecs(0).into(),
574            ..Default::default()
575        });
576
577        let mut provider = FernetTokenProvider::new(setup_config());
578        provider.load_keys().unwrap();
579
580        let encrypted = provider.encrypt(&token).unwrap();
581        let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap());
582        assert_eq!(token, dec_token);
583    }
584
585    #[tokio::test]
586    async fn test_decrypt_federation_unscoped() {
587        let token = "gAAAAABoMdfwBgwjAfYCp3RisL_XKSdGKmBqg7ia8jkfsKIXnap_bQ5gUTZGwgEERlpFKzbwpkV-cpiFDuhe9RAnCtbQxEhP7Rg1vt1VLm8afGTulDaLclqot2NC-BONFO2k3V3KyIa-Xrq0mCEGOk-BhNZy2C6iwrWanPCjCuZrWCq4FBirtMs2vrnZPWG5FTGqqkvdQvGj";
588
589        let mut provider = FernetTokenProvider::new(setup_config());
590        provider.load_keys().unwrap();
591
592        if let Token::FederationUnscoped(decrypted) = provider.decrypt(token).unwrap() {
593            assert_eq!(decrypted.user_id, "8980e124df5245509131bdc5c66c54cc");
594            assert_eq!(decrypted.methods, vec!["openid"]);
595            assert_eq!(
596                decrypted.expires_at.to_rfc3339(),
597                "2025-05-24T16:30:03+00:00"
598            );
599            assert_eq!(
600                decrypted.audit_ids,
601                vec!["3622030ded92477095dadcde340770e5"]
602            );
603            assert_eq!(decrypted.idp_id, "idp_id");
604            assert_eq!(decrypted.protocol_id, "oidc");
605            assert_eq!(decrypted.group_ids, vec!["g1", "g2"]);
606        } else {
607            panic!()
608        }
609    }
610
611    #[tokio::test]
612    async fn test_federation_unscoped_roundtrip() {
613        let token = Token::FederationUnscoped(FederationUnscopedPayload {
614            user_id: Uuid::new_v4().simple().to_string(),
615            methods: vec!["password".into()],
616            group_ids: vec!["g1".into()],
617            idp_id: "idp_id".into(),
618            protocol_id: "proto".into(),
619
620            audit_ids: vec!["Zm9vCg".into()],
621            expires_at: Local::now().trunc_subsecs(0).into(),
622            ..Default::default()
623        });
624
625        let mut provider = FernetTokenProvider::new(setup_config());
626        provider.load_keys().unwrap();
627
628        let encrypted = provider.encrypt(&token).unwrap();
629        let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap());
630        assert_eq!(token, dec_token);
631    }
632
633    #[tokio::test]
634    async fn test_decrypt_federation_project_scope() {
635        let token = "gAAAAABoNdYE5zCP0qQtHqhdbZHQ7YdLvfDlUTpLou8FJFoMKsd4I9jyVyaWrluYXKXofnwzemA-wybhtbNruwqDYH-wmHdMlgYuZyy21o8ylphU5yd2b-5KvGpXo61fTVTzhdHFTzJKVit_7Lcwq0S45xQ9x14sVRd870NEwfmOvUVR5BGzmnpFLvWtkaPSpbxMAzfn_NSC";
636
637        let mut provider = FernetTokenProvider::new(setup_config());
638        provider.load_keys().unwrap();
639
640        if let Token::FederationProjectScope(decrypted) = provider.decrypt(token).unwrap() {
641            assert_eq!(decrypted.user_id, "8980e124df5245509131bdc5c66c54cc");
642            assert_eq!(decrypted.methods, vec!["openid"]);
643            assert_eq!(
644                decrypted.expires_at.to_rfc3339(),
645                "2025-05-27T17:11:00+00:00",
646            );
647            assert_eq!(
648                decrypted.audit_ids,
649                vec!["dcbf4d403b7a45dca32d029d54c953d9"]
650            );
651            assert_eq!(decrypted.project_id, "pid");
652            assert_eq!(decrypted.idp_id, "idp_id");
653            assert_eq!(decrypted.protocol_id, "oidc");
654            assert_eq!(decrypted.group_ids, vec!["g1", "g2"]);
655        } else {
656            panic!()
657        }
658    }
659
660    #[tokio::test]
661    async fn test_federation_project_scope_roundtrip() {
662        let token = Token::FederationProjectScope(FederationProjectScopePayload {
663            user_id: Uuid::new_v4().simple().to_string(),
664            methods: vec!["password".into()],
665            project_id: "pid".into(),
666            group_ids: vec!["g1".into()],
667            idp_id: "idp_id".into(),
668            protocol_id: "proto".into(),
669            audit_ids: vec!["Zm9vCg".into()],
670            expires_at: Local::now().trunc_subsecs(0).into(),
671            ..Default::default()
672        });
673
674        let mut provider = FernetTokenProvider::new(setup_config());
675        provider.load_keys().unwrap();
676
677        let encrypted = provider.encrypt(&token).unwrap();
678        let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap());
679        assert_eq!(token, dec_token);
680    }
681
682    #[tokio::test]
683    async fn test_decrypt_federation_domain_scope() {
684        let token = "gAAAAABoNddwFaB2Oq26-4f8nRK3Bph7-QsIh30Rbefbb78owJXaQcjNQm5Qq1gHouS6JSqgfpdna3ML1vdTVnVnFScX-T-CZ-CqtBPUuEBHFEzdNBDKQHloYajZ2sknwbe_uIs1SDS9tBFLvkVth1eVjDhdEawINHjUCFhNPObZKas5V0j7bsvChNeZBKsznruJwCtcrWr5";
685
686        let mut provider = FernetTokenProvider::new(setup_config());
687        provider.load_keys().unwrap();
688
689        if let Token::FederationDomainScope(decrypted) = provider.decrypt(token).unwrap() {
690            assert_eq!(decrypted.user_id, "8980e124df5245509131bdc5c66c54cc");
691            assert_eq!(decrypted.methods, vec!["openid"]);
692            assert_eq!(
693                decrypted.expires_at.to_rfc3339(),
694                "2025-05-27T17:17:04+00:00",
695            );
696            assert_eq!(
697                decrypted.audit_ids,
698                vec!["ab892135f51240f5bae8ec7179873bf6"]
699            );
700            assert_eq!(decrypted.domain_id, "did");
701            assert_eq!(decrypted.idp_id, "idp_id");
702            assert_eq!(decrypted.protocol_id, "oidc");
703            assert_eq!(decrypted.group_ids, vec!["g1", "g2"]);
704        } else {
705            panic!()
706        }
707    }
708
709    #[tokio::test]
710    async fn test_federation_domain_scope_roundtrip() {
711        let token = Token::FederationDomainScope(FederationDomainScopePayload {
712            user_id: Uuid::new_v4().simple().to_string(),
713            methods: vec!["password".into()],
714            domain_id: "pid".into(),
715            group_ids: vec!["g1".into()],
716            idp_id: "idp_id".into(),
717            protocol_id: "proto".into(),
718            audit_ids: vec!["Zm9vCg".into()],
719            expires_at: Local::now().trunc_subsecs(0).into(),
720            ..Default::default()
721        });
722
723        let config = crate::tests::token::setup_config();
724        let mut provider = FernetTokenProvider::new(config);
725        provider.load_keys().unwrap();
726
727        let encrypted = provider.encrypt(&token).unwrap();
728        let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap());
729        assert_eq!(token, dec_token);
730    }
731
732    #[tokio::test]
733    async fn test_decrypt_application_credential() {
734        let token = "gAAAAABnt11m57ZlI9JU0g2BKJw2EN-InbAIijcIG7SxvPATntgTlcTMwha-Fh7isNNIwDq2WaWglV1nYgftfoUK245ZnEJ0_gXaIhl6COhNommYv2Bs9PnJqfgrrxrIrB8rh4pfeyCtMkv5ePYgFFPyRFE37l3k7qL5p7qVhYT37yT1-K5lYAV0f6Vy70h3KX1HO0m6Rl90";
735
736        let mut provider = FernetTokenProvider::new(setup_config());
737        provider.load_keys().unwrap();
738
739        if let Token::ApplicationCredential(decrypted) = provider.decrypt(token).unwrap() {
740            assert_eq!(decrypted.user_id, "4b7d364ad87d400bbd91798e3c15e9c2");
741            assert_eq!(decrypted.project_id, "97cd761d581b485792a4afc8cc6a998d");
742            assert_eq!(decrypted.methods, vec!["application_credential"]);
743            assert_eq!(
744                decrypted.expires_at.to_rfc3339(),
745                "2025-02-20T17:50:46+00:00"
746            );
747            assert_eq!(decrypted.audit_ids, vec!["kD7Cwc8fSZuWNPZhy0fLVg"]);
748            assert_eq!(
749                decrypted.application_credential_id,
750                "a67630c36e1b48839091c905177c5598"
751            );
752        } else {
753            panic!()
754        }
755    }
756
757    #[tokio::test]
758    async fn test_application_credential_roundtrip() {
759        let token = Token::ApplicationCredential(ApplicationCredentialPayload {
760            user_id: Uuid::new_v4().simple().to_string(),
761            methods: vec!["application_credential".into()],
762            project_id: Uuid::new_v4().simple().to_string(),
763            application_credential_id: Uuid::new_v4().simple().to_string(),
764            audit_ids: vec!["Zm9vCg".into()],
765            expires_at: Local::now().trunc_subsecs(0).into(),
766            ..Default::default()
767        });
768
769        let mut provider = FernetTokenProvider::new(setup_config());
770        provider.load_keys().unwrap();
771
772        let encrypted = provider.encrypt(&token).unwrap();
773        let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap());
774        assert_eq!(token, dec_token);
775    }
776
777    #[tokio::test]
778    async fn test_restricted_roundtrip() {
779        let token = Token::Restricted(RestrictedPayload {
780            user_id: Uuid::new_v4().simple().to_string(),
781            methods: vec!["password".into()],
782            token_restriction_id: Uuid::new_v4().simple().to_string(),
783            project_id: Uuid::new_v4().simple().to_string(),
784            allow_renew: true,
785            allow_rescope: true,
786            audit_ids: vec!["Zm9vCg".into()],
787            expires_at: Local::now().trunc_subsecs(0).into(),
788            ..Default::default()
789        });
790
791        let mut provider = FernetTokenProvider::new(setup_config());
792        provider.load_keys().unwrap();
793
794        let encrypted = provider.encrypt(&token).unwrap();
795        let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap());
796        assert_eq!(token, dec_token);
797    }
798
799    #[tokio::test]
800    async fn test_trust_roundtrip() {
801        let token = Token::Trust(TrustPayload {
802            user_id: Uuid::new_v4().simple().to_string(),
803            methods: vec!["password".into()],
804            trust_id: Uuid::new_v4().simple().to_string(),
805            project_id: Uuid::new_v4().simple().to_string(),
806            audit_ids: vec!["Zm9vCg".into()],
807            expires_at: Local::now().trunc_subsecs(0).into(),
808            ..Default::default()
809        });
810
811        let mut provider = FernetTokenProvider::new(setup_config());
812        provider.load_keys().unwrap();
813
814        let encrypted = provider.encrypt(&token).unwrap();
815        let dec_token = discard_issued_at(provider.decrypt(&encrypted).unwrap());
816        assert_eq!(token, dec_token);
817    }
818}