atomic_lti/
id_token.rs

1use crate::jwt::insecure_decode;
2use crate::lti_definitions::{LTI_DEEP_LINKING_REQUEST, RESOURCE_LINK_CLAIM};
3use crate::names_and_roles::NamesAndRolesClaim;
4use crate::{errors::SecureError, lti_definitions::NAMES_AND_ROLES_SERVICE_VERSIONS};
5use chrono::{Duration, Utc};
6use serde::de::{self, Unexpected};
7use serde::{Deserialize, Deserializer, Serialize};
8use serde_with::skip_serializing_none;
9use std::collections::HashMap;
10use strum_macros::EnumString;
11
12#[derive(Debug, Deserialize, Serialize, Clone)]
13pub struct IdTokenErrors {
14  errors: Option<Errors>,
15}
16
17#[derive(Debug, Deserialize, Serialize, Clone)]
18pub struct Errors {
19  errors: Option<()>,
20}
21
22#[derive(Debug, PartialEq, EnumString, Deserialize, Serialize, Clone)]
23#[strum(ascii_case_insensitive)]
24pub enum DocumentTargets {
25  #[serde(rename = "iframe")]
26  Iframe,
27  #[serde(rename = "window")]
28  Window,
29  #[serde(rename = "embed")]
30  Embed,
31}
32
33#[derive(Debug, PartialEq, EnumString, Deserialize, Serialize, Clone)]
34#[strum(ascii_case_insensitive)]
35pub enum AcceptTypes {
36  #[serde(rename = "link")]
37  Link,
38  #[serde(rename = "file")]
39  File,
40  #[serde(rename = "html")]
41  Html,
42  #[serde(rename = "ltiResourceLink")]
43  #[serde(
44    alias = "ltiResourceLink",
45    alias = "ltiresourcelink",
46    alias = "LtiResourceLink"
47  )]
48  LtiResourceLink,
49  #[serde(rename = "image")]
50  Image,
51}
52
53// https://www.imsglobal.org/spec/lti/v1p3#resource-link-claim
54#[skip_serializing_none]
55#[derive(Debug, Deserialize, Serialize, Clone)]
56pub struct ResourceLinkClaim {
57  // Opaque identifier for a placement of an LTI resource link within a context that MUST
58  // be a stable and locally unique to the deployment_id. This value MUST change if the link
59  // is copied or exported from one system or context and imported into another system or context.
60  // The value of id MUST NOT exceed 255 ASCII characters in length and is case-sensitive
61  pub id: String,
62  // Descriptive phrase for an LTI resource link placement.
63  pub description: Option<String>,
64  // Descriptive title for an LTI resource link placement.
65  pub title: Option<String>,
66  pub validation_context: Option<String>,
67  pub errors: Option<IdTokenErrors>,
68}
69
70// https://www.imsglobal.org/spec/lti/v1p3#launch-presentation-claim
71#[skip_serializing_none]
72#[derive(Debug, Deserialize, Serialize, Clone)]
73pub struct LaunchPresentationClaim {
74  // The kind of browser window or frame from which the user launched inside the message
75  // sender's system. The value for this property MUST be one of: frame, iframe, or window.
76  pub document_target: Option<DocumentTargets>,
77  // Fully-qualified HTTPS URL within the message sender's user experience to where the message
78  // receiver can redirect the user back. The message receiver can redirect to this URL after
79  // the user has finished activity, or if the receiver cannot start because of some technical difficulty.
80  pub return_url: Option<String>,
81  // Language, country, and variant as represented using the IETF Best Practices for Tags for Identifying Languages
82  pub locale: String,
83  // Height of the window or frame where the content from the message receiver will be displayed to the user.
84  pub height: Option<i32>,
85  // Width of the window or frame where the content from the message receiver will be displayed to the user.
86  pub width: Option<i32>,
87  pub validation_context: Option<String>,
88  pub errors: Option<IdTokenErrors>,
89}
90
91#[skip_serializing_none]
92#[derive(Debug, Deserialize, Serialize, Clone)]
93pub struct DeepLinkingClaim {
94  pub deep_link_return_url: String,
95  pub accept_types: Vec<AcceptTypes>,
96  pub accept_presentation_document_targets: Vec<DocumentTargets>,
97  pub accept_media_types: Option<String>,
98  pub accept_multiple: Option<bool>,
99  pub accept_lineitem: Option<bool>,
100  pub auto_create: Option<bool>,
101  pub title: Option<String>,
102  pub text: Option<String>,
103  pub data: Option<String>,
104}
105
106#[derive(Debug, PartialEq, EnumString, Deserialize, Serialize, Clone)]
107pub enum AGSScopes {
108  #[serde(rename = "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem")]
109  LineItem,
110  #[serde(rename = "https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly")]
111  ResultReadOnly,
112  #[serde(rename = "https://purl.imsglobal.org/spec/lti-ags/scope/score")]
113  Score,
114  #[serde(rename = "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly")]
115  LineItemReadOnly,
116}
117
118#[skip_serializing_none]
119#[derive(Debug, Deserialize, Serialize, Clone)]
120pub struct AGSClaim {
121  pub scope: Vec<AGSScopes>,
122  pub lineitems: String,
123  pub lineitem: Option<String>,
124  pub validation_context: Option<String>,
125  pub errors: Option<IdTokenErrors>,
126}
127
128#[skip_serializing_none]
129#[derive(Debug, Deserialize, Serialize, Clone)]
130pub struct LISClaim {
131  pub person_sourcedid: String,
132  pub course_offering_sourcedid: Option<String>,
133  pub course_section_sourcedid: Option<String>,
134  pub validation_context: Option<String>,
135  pub errors: Option<IdTokenErrors>,
136}
137
138#[skip_serializing_none]
139#[derive(Debug, Deserialize, Serialize, Clone)]
140pub struct ContextClaim {
141  pub id: String,
142  pub label: Option<String>,
143  pub title: Option<String>,
144  pub r#type: Option<Vec<String>>,
145  pub validation_context: Option<String>,
146  pub errors: Option<IdTokenErrors>,
147}
148
149#[skip_serializing_none]
150#[derive(Debug, Deserialize, Serialize, Clone)]
151pub struct ToolPlatformClaim {
152  pub guid: String,
153  pub contact_email: Option<String>,
154  pub description: Option<String>,
155  pub name: Option<String>,
156  pub url: Option<String>,
157  pub product_family_code: Option<String>,
158  pub version: Option<String>,
159  pub validation_context: Option<String>,
160  pub errors: Option<IdTokenErrors>,
161}
162
163// Custom deserializer for handling "aud" being either a string or an array of strings.
164// If it is an array, it will return the first element.
165fn aud_deserializer<'de, D>(deserializer: D) -> Result<String, D::Error>
166where
167  D: Deserializer<'de>,
168{
169  let aud: serde_json::Value = Deserialize::deserialize(deserializer)?;
170  match aud {
171    serde_json::Value::String(s) => {
172      dbg!(&s);
173      Ok(s)
174    }
175    serde_json::Value::Array(arr) => {
176      if let Some(serde_json::Value::String(first_aud)) = arr.first() {
177        Ok(first_aud.clone())
178      } else {
179        Err(de::Error::invalid_type(
180          Unexpected::Seq,
181          &"a string or an array of strings",
182        ))
183      }
184    }
185    _ => Err(de::Error::invalid_type(
186      Unexpected::Other("non-string"),
187      &"a string or an array of strings",
188    )),
189  }
190}
191
192// Custom deserializer for handling "aud" being an array of strings and populating "auds".
193// If "aud" is a string, we don't populate "auds".
194fn auds_deserializer<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
195where
196  D: Deserializer<'de>,
197{
198  let aud: serde_json::Value = Deserialize::deserialize(deserializer)?;
199  match aud {
200    // If "aud" is a string, we don't populate "auds"
201    serde_json::Value::String(_) => Ok(None),
202    // If "aud" is an array, we populate "auds"
203    serde_json::Value::Array(arr) => {
204      let mut auds = Vec::new();
205      for elem in arr {
206        if let serde_json::Value::String(s) = elem {
207          auds.push(s);
208        } else {
209          return Err(de::Error::invalid_type(
210            Unexpected::Other("non-string element in array"),
211            &"an array of strings",
212          ));
213        }
214      }
215      Ok(Some(auds))
216    }
217    // If "aud" is neither a string nor an array, return an error
218    _ => Err(de::Error::invalid_type(
219      Unexpected::Other("non-string"),
220      &"a string or an array of strings",
221    )),
222  }
223}
224
225#[skip_serializing_none]
226#[derive(Debug, Deserialize, Serialize, Clone)]
227pub struct IdToken {
228  #[serde(default, deserialize_with = "aud_deserializer")]
229  pub aud: String,
230  #[serde(default, deserialize_with = "auds_deserializer")]
231  pub auds: Option<Vec<String>>,
232  #[serde(default)]
233  pub azp: Option<String>,
234  pub exp: i64,
235  pub iat: i64,
236  pub iss: String,
237  pub nonce: String,
238  pub sub: String,
239
240  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/message_type")]
241  pub message_type: String,
242  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/version")]
243  pub lti_version: String,
244  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/resource_link")]
245  pub resource_link: Option<ResourceLinkClaim>,
246  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/deployment_id")]
247  pub deployment_id: String,
248  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/target_link_uri")]
249  pub target_link_uri: String,
250  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/roles")]
251  pub roles: Vec<String>,
252  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/role_scope_mentor")]
253  pub role_scope_mentor: Option<Vec<String>>,
254  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/context")]
255  pub context: Option<ContextClaim>,
256  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/tool_platform")]
257  pub tool_platform: Option<ToolPlatformClaim>,
258  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/launch_presentation")]
259  pub launch_presentation: Option<LaunchPresentationClaim>,
260  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/lis")]
261  pub lis: Option<LISClaim>,
262  // TODO need to figure out how to handle this
263  // #[serde(rename = "http://www.ExamplePlatformVendor.com/session")]
264  // pub session: Option<HashMap<String, String>>,
265  #[serde(rename = "https://purl.imsglobal.org/spec/lti/claim/custom")]
266  pub custom: Option<HashMap<String, String>>,
267  #[serde(rename = "https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings")]
268  pub deep_linking: Option<DeepLinkingClaim>,
269  #[serde(rename = "https://purl.imsglobal.org/spec/lti-dl/claim/data ")]
270  pub data: Option<String>,
271  #[serde(rename = "https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice")]
272  pub names_and_roles: Option<NamesAndRolesClaim>,
273  #[serde(rename = "https://purl.imsglobal.org/spec/lti-ags/claim/endpoint")]
274  pub ags: Option<AGSClaim>,
275
276  pub lti11_legacy_user_id: Option<String>,
277  pub picture: Option<String>,
278  pub email: Option<String>,
279  pub name: Option<String>,
280  pub given_name: Option<String>,
281  pub family_name: Option<String>,
282  pub middle_name: Option<String>,
283  pub locale: Option<String>,
284
285  pub errors: Option<IdTokenErrors>,
286}
287
288impl IdToken {
289  // Extract the iss claim from an ID Token without validating the signature
290  pub fn extract_iss(token: &str) -> Result<String, SecureError> {
291    let id_token = insecure_decode::<IdToken>(token)?.claims;
292    Ok(id_token.iss)
293  }
294
295  // Get the client_id from the IdToken
296  pub fn client_id(&self) -> String {
297    match &self.auds {
298      Some(auds) if auds.len() > 1 => {
299        // If multiple auds exist and azp is present, return azp
300        self.azp.clone().unwrap_or_else(|| auds[0].clone())
301      }
302      Some(auds) => {
303        // If only one aud is present, return it
304        auds[0].clone()
305      }
306      None => {
307        // If no auds are present, fall back to aud
308        self.aud.clone()
309      }
310    }
311  }
312
313  // Returns the LMS host URL from the IdToken, based on whether it's a deep link launch or not
314  pub fn lms_host(&self) -> Option<String> {
315    if self.is_deep_link_launch() {
316      self
317        .deep_linking
318        .as_ref()
319        .map(|dl| dl.deep_link_return_url.clone())
320    } else {
321      self
322        .launch_presentation
323        .as_ref()
324        .and_then(|lp| lp.return_url.clone())
325    }
326  }
327
328  // Returns the LMS URL from the IdToken, based on the LMS host URL
329  pub fn lms_url(&self) -> Option<String> {
330    self.lms_host().map(|host| format!("https://{}", host))
331  }
332
333  // Returns true if the IdToken is a deep link launch
334  pub fn is_deep_link_launch(&self) -> bool {
335    self.message_type == LTI_DEEP_LINKING_REQUEST
336  }
337
338  // Returns true if the IdToken is a names and roles launch
339  pub fn is_names_and_roles_launch(&self) -> bool {
340    match &self.names_and_roles {
341      None => false,
342      Some(nar) => {
343        if !nar
344          .service_versions
345          .iter()
346          .any(|v| v == NAMES_AND_ROLES_SERVICE_VERSIONS[0])
347        {
348          return false;
349        }
350        true
351      }
352    }
353  }
354
355  // Returns the names and roles endpoint URL from the IdToken
356  pub fn names_and_roles_endpoint(&self) -> Option<String> {
357    self
358      .names_and_roles
359      .as_ref()
360      .map(|nar| nar.context_memberships_url.clone())
361  }
362
363  // Returns true if the IdToken is an assignment and grades launch
364  pub fn is_assignment_and_grades_launch(&self) -> bool {
365    self.ags.is_some()
366  }
367
368  // Validates the contents of the IdToken and returns a vector of errors
369  pub fn validate(&self, requested_target_link_uri: &str) -> Vec<String> {
370    let mut errors = Vec::new();
371
372    if self.target_link_uri != requested_target_link_uri {
373      errors.push(format!(
374        "LTI token target link uri '{}' doesn't match url '{}'",
375        self.target_link_uri, requested_target_link_uri
376      ));
377    }
378
379    if let Some(resource_link) = &self.resource_link {
380      if resource_link.id.is_empty() {
381        errors.push(format!(
382          "LTI token is missing required field id from the claim {}",
383          RESOURCE_LINK_CLAIM
384        ));
385      }
386    }
387
388    if let Some(auds) = &self.auds {
389      if auds.len() > 1 {
390        if self.azp.is_none() {
391          errors.push("LTI token is missing required field azp".to_string());
392        } else if !auds.contains(&self.azp.clone().unwrap_or_default()) {
393          errors.push("azp is not one of the aud's".to_owned());
394        }
395      }
396    }
397
398    if !self.lti_version.starts_with("1.3") {
399      errors.push("Invalid LTI version".to_owned());
400    }
401
402    errors
403  }
404
405  // Create a new id token that can be embedded in a JWT that will be sent to the client
406  // The entire JWT needs to fit in a HTTP header field so we strip out
407  // known large values that won't be needed by the API endpoints
408  pub fn to_client_id_token(&self) -> IdToken {
409    let mut id_token = self.clone();
410    id_token.launch_presentation = None;
411
412    // token.delete(AtomicLti::Definitions::CUSTOM_CLAIM)
413    // token.delete(AtomicLti::Definitions::LAUNCH_PRESENTATION)
414    // token.delete(AtomicLti::Definitions::BASIC_OUTCOME_CLAIM)
415    // token.delete(AtomicLti::Definitions::ROLES_CLAIM)
416    // token[AtomicLti::Definitions::RESOURCE_LINK_CLAIM]&.delete("description")
417
418    id_token
419  }
420}
421
422impl Default for IdToken {
423  fn default() -> Self {
424    Self {
425      aud: "".to_string(),
426      auds: None,
427      azp: None,
428      exp: (Utc::now() + Duration::minutes(15)).timestamp(),
429      iat: Utc::now().timestamp(),
430      iss: "".to_string(),
431      nonce: "".to_string(),
432      sub: "".to_string(),
433      message_type: "".to_string(),
434      lti_version: "".to_string(),
435      resource_link: Some(ResourceLinkClaim {
436        id: "".to_string(),
437        description: None,
438        title: None,
439        validation_context: None,
440        errors: None,
441      }),
442      deployment_id: "".to_string(),
443      target_link_uri: "".to_string(),
444      roles: vec![],
445      role_scope_mentor: Some(vec![]),
446      context: None,
447      tool_platform: None,
448      launch_presentation: None,
449      lis: None,
450      //session: HashMap::new(),
451      custom: None,
452      deep_linking: None,
453      data: None,
454      names_and_roles: None,
455      ags: None,
456      lti11_legacy_user_id: None,
457      picture: None,
458      email: None,
459      name: None,
460      given_name: None,
461      family_name: None,
462      middle_name: None,
463      locale: None,
464      errors: None,
465    }
466  }
467}
468
469#[cfg(test)]
470mod tests {
471  use super::*;
472  use crate::jwks::{decode, encode, generate_jwk, Jwks};
473  use jsonwebtoken::jwk::JwkSet;
474  use openssl::rsa::Rsa;
475
476  #[test]
477  fn test_id_token_incorrect_target_link_uri() {
478    let id_token = IdToken {
479      target_link_uri: "notexample.com".to_string(),
480      resource_link: Some(ResourceLinkClaim {
481        id: "123".to_string(),
482        description: None,
483        title: None,
484        validation_context: None,
485        errors: None,
486      }),
487      auds: Some(vec!["example.com".to_string()]),
488      azp: Some("".to_string()),
489      aud: "example.com".to_string(),
490      lti_version: "1.3".to_string(),
491      ..Default::default()
492    };
493    let errors = id_token.validate("example.com");
494    assert_eq!(errors.len(), 1);
495  }
496
497  #[test]
498  fn test_lms_host() {
499    let id_token = IdToken {
500      message_type: LTI_DEEP_LINKING_REQUEST.to_string(),
501      deep_linking: Some(DeepLinkingClaim {
502        deep_link_return_url: "example.com".to_string(),
503        accept_types: vec![AcceptTypes::Link],
504        accept_presentation_document_targets: vec![DocumentTargets::Iframe],
505        accept_media_types: None,
506        accept_multiple: None,
507        accept_lineitem: None,
508        auto_create: None,
509        title: None,
510        text: None,
511        data: None,
512      }),
513      launch_presentation: None,
514      ..Default::default()
515    };
516    assert_eq!(id_token.lms_host(), Some("example.com".to_string()));
517  }
518
519  #[test]
520  fn test_lms_url() {
521    let id_token = IdToken {
522      message_type: LTI_DEEP_LINKING_REQUEST.to_string(),
523      deep_linking: Some(DeepLinkingClaim {
524        deep_link_return_url: "example.com".to_string(),
525        accept_types: vec![AcceptTypes::Link],
526        accept_presentation_document_targets: vec![DocumentTargets::Iframe],
527        accept_media_types: None,
528        accept_multiple: None,
529        accept_lineitem: None,
530        auto_create: None,
531        title: None,
532        text: None,
533        data: None,
534      }),
535      launch_presentation: None,
536      ..Default::default()
537    };
538    assert_eq!(id_token.lms_url(), Some("https://example.com".to_string()));
539  }
540
541  #[test]
542  fn test_is_deep_link_launch() {
543    let id_token = IdToken {
544      message_type: LTI_DEEP_LINKING_REQUEST.to_string(),
545      ..Default::default()
546    };
547    assert!(id_token.is_deep_link_launch());
548  }
549
550  #[test]
551  fn test_is_names_and_roles_launch() {
552    let id_token = IdToken {
553      names_and_roles: Some(NamesAndRolesClaim {
554        context_memberships_url: "example.com".to_string(),
555        service_versions: vec![NAMES_AND_ROLES_SERVICE_VERSIONS[0].to_string()],
556        validation_context: None,
557        errors: None,
558      }),
559      ..Default::default()
560    };
561    assert!(id_token.is_names_and_roles_launch());
562  }
563
564  #[test]
565  fn test_is_assignment_and_grades_launch() {
566    let id_token = IdToken {
567      ags: Some(AGSClaim {
568        scope: vec![AGSScopes::LineItem],
569        lineitems: "example.com".to_string(),
570        lineitem: None,
571        validation_context: None,
572        errors: None,
573      }),
574      ..Default::default()
575    };
576    assert!(id_token.is_assignment_and_grades_launch());
577  }
578
579  #[test]
580  fn test_validate() {
581    let id_token = IdToken {
582      target_link_uri: "example.com".to_string(),
583      resource_link: Some(ResourceLinkClaim {
584        id: "123".to_string(),
585        description: None,
586        title: None,
587        validation_context: None,
588        errors: None,
589      }),
590      auds: Some(vec!["example.com".to_string()]),
591      azp: Some("".to_string()),
592      aud: "example.com".to_string(),
593      lti_version: "1.3".to_string(),
594      ..Default::default()
595    };
596    let errors = id_token.validate("example.com");
597    assert_eq!(errors.len(), 0);
598  }
599
600  #[test]
601  fn test_extract_iss() {
602    let iss = "https://lms.example.com";
603    let aud = "1234";
604    let user_id = "12";
605    let rsa_key_pair = Rsa::generate(2048).expect("Failed to generate RSA key");
606    let kid = "asdf_kid";
607    let jwk = generate_jwk(kid, &rsa_key_pair).expect("Failed to generate JWK");
608
609    // Set the expiration time to 15 minutes from now
610    let expiration = Utc::now() + Duration::minutes(15);
611
612    // Create a sample ID Token with an iss claim
613    let id_token = IdToken {
614      iss: iss.to_string(),
615      sub: user_id.to_string(),
616      aud: aud.to_string(),
617      exp: expiration.timestamp(),
618      message_type: LTI_DEEP_LINKING_REQUEST.to_string(),
619      deep_linking: Some(DeepLinkingClaim {
620        deep_link_return_url: "example.com".to_string(),
621        accept_types: vec![AcceptTypes::Link],
622        accept_presentation_document_targets: vec![DocumentTargets::Iframe],
623        accept_media_types: None,
624        accept_multiple: None,
625        accept_lineitem: None,
626        auto_create: None,
627        title: None,
628        text: None,
629        data: None,
630      }),
631      launch_presentation: None,
632      ..Default::default()
633    };
634
635    // Encode the ID Token using the private key
636    let token = encode(&id_token, &jwk.kid, rsa_key_pair).expect("Failed to encode token");
637
638    // Test the extract_iss function
639    let extracted_iss = IdToken::extract_iss(&token).unwrap();
640    assert_eq!(extracted_iss, iss);
641  }
642
643  #[test]
644  fn test_decode() {
645    let iss = "https://lms.example.com";
646    let aud = "1234";
647    let user_id = "12";
648    let rsa_key_pair = Rsa::generate(2048).expect("Failed to generate RSA key");
649    let kid = "asdf_kid";
650    let jwk = generate_jwk(kid, &rsa_key_pair).expect("Failed to generate JWK");
651    let jwks = Jwks {
652      keys: vec![jwk.clone()],
653    };
654    let jwks_json = serde_json::to_string(&jwks).expect("Failed to generate JSON for JWKS");
655
656    // Set the expiration time to 15 minutes from now
657    let expiration = Utc::now() + Duration::minutes(15);
658
659    // Create a sample ID Token with an iss claim
660    let id_token = IdToken {
661      iss: iss.to_string(),
662      sub: user_id.to_string(),
663      aud: aud.to_string(),
664      exp: expiration.timestamp(),
665      message_type: LTI_DEEP_LINKING_REQUEST.to_string(),
666      deep_linking: Some(DeepLinkingClaim {
667        deep_link_return_url: "example.com".to_string(),
668        accept_types: vec![AcceptTypes::Link],
669        accept_presentation_document_targets: vec![DocumentTargets::Iframe],
670        accept_media_types: None,
671        accept_multiple: None,
672        accept_lineitem: None,
673        auto_create: None,
674        title: None,
675        text: None,
676        data: None,
677      }),
678      launch_presentation: None,
679      ..Default::default()
680    };
681
682    // Encode the ID Token using the private key
683    let token = encode(&id_token, &jwk.kid, rsa_key_pair).expect("Failed to encode token");
684
685    // Test the decode function with an id token
686    let jwk_set: JwkSet = serde_json::from_str(&jwks_json).expect("Failed to parse JWKS");
687    let extracted_id_token = decode(&token, &jwk_set).expect("Failed to decode token");
688    assert_eq!(extracted_id_token.aud, aud);
689  }
690}