@@ -180,6 +180,7 @@ pub(crate) mod dynamic {
180180/// [AIP-4110]: https://google.aip.dev/auth/4110
181181pub struct Builder {
182182 target_audience : String ,
183+ include_email : bool ,
183184}
184185
185186impl Builder {
@@ -193,9 +194,20 @@ impl Builder {
193194 pub fn new < S : Into < String > > ( target_audience : S ) -> Self {
194195 Self {
195196 target_audience : target_audience. into ( ) ,
197+ include_email : false ,
196198 }
197199 }
198200
201+ /// Sets whether the ID token should include the `email` claim of the user in the token.
202+ ///
203+ /// For some credentials sources like Metadata Server and Impersonated Credentials, the default is
204+ /// to not include the `email` claim. For other sources, they always include it.
205+ /// This option is only relevant for credentials sources that do not include the `email` claim by default.
206+ pub fn with_include_email ( mut self ) -> Self {
207+ self . include_email = true ;
208+ self
209+ }
210+
199211 /// Returns a [IDTokenCredentials] instance with the configured settings.
200212 ///
201213 /// # Errors
@@ -214,30 +226,62 @@ impl Builder {
214226 AdcContents :: FallbackToMds => None ,
215227 } ;
216228
217- build_id_token_credentials ( self . target_audience , json_data)
229+ build_id_token_credentials ( self . target_audience , self . include_email , json_data)
218230 }
219231}
232+ enum IDTokenBuilder {
233+ Mds ( mds:: Builder ) ,
234+ ServiceAccount ( service_account:: Builder ) ,
235+ Impersonated ( impersonated:: Builder ) ,
236+ }
220237
221238fn build_id_token_credentials (
222239 audience : String ,
240+ include_email : bool ,
223241 json : Option < Value > ,
224242) -> BuildResult < IDTokenCredentials > {
243+ let builder = build_id_token_credentials_internal ( audience, include_email, json) ?;
244+ match builder {
245+ IDTokenBuilder :: Mds ( builder) => builder. build ( ) ,
246+ IDTokenBuilder :: ServiceAccount ( builder) => builder. build ( ) ,
247+ IDTokenBuilder :: Impersonated ( builder) => builder. build ( ) ,
248+ }
249+ }
250+
251+ fn build_id_token_credentials_internal (
252+ audience : String ,
253+ include_email : bool ,
254+ json : Option < Value > ,
255+ ) -> BuildResult < IDTokenBuilder > {
225256 match json {
226257 None => {
227258 // TODO(#3587): pass context that is being built from ADC flow.
228- mds:: Builder :: new ( audience)
229- . with_format ( mds:: Format :: Full )
230- . build ( )
259+ let format = if include_email {
260+ mds:: Format :: Full
261+ } else {
262+ mds:: Format :: Standard
263+ } ;
264+ Ok ( IDTokenBuilder :: Mds (
265+ mds:: Builder :: new ( audience) . with_format ( format) ,
266+ ) )
231267 }
232268 Some ( json) => {
233269 let cred_type = extract_credential_type ( & json) ?;
234270 match cred_type {
235271 "authorized_user" => Err ( BuilderError :: not_supported ( format ! (
236272 "{cred_type}, use idtoken::user_account::Builder directly."
237273 ) ) ) ,
238- "service_account" => service_account:: Builder :: new ( audience, json) . build ( ) ,
274+ "service_account" => Ok ( IDTokenBuilder :: ServiceAccount (
275+ service_account:: Builder :: new ( audience, json) ,
276+ ) ) ,
239277 "impersonated_service_account" => {
240- impersonated:: Builder :: new ( audience, json) . build ( )
278+ let builder = impersonated:: Builder :: new ( audience, json) ;
279+ let builder = if include_email {
280+ builder. with_include_email ( )
281+ } else {
282+ builder
283+ } ;
284+ Ok ( IDTokenBuilder :: Impersonated ( builder) )
241285 }
242286 "external_account" => {
243287 // never gonna be supported for id tokens
@@ -289,7 +333,9 @@ fn instant_from_epoch_seconds(secs: u64, now: SystemTime) -> Option<Instant> {
289333pub ( crate ) mod tests {
290334 use super :: * ;
291335 use jsonwebtoken:: { Algorithm , EncodingKey , Header } ;
336+ use mds:: Format ;
292337 use rsa:: pkcs1:: EncodeRsaPrivateKey ;
338+ use serde_json:: json;
293339 use serial_test:: parallel;
294340 use std:: collections:: HashMap ;
295341 use std:: time:: { Duration , SystemTime , UNIX_EPOCH } ;
@@ -380,7 +426,7 @@ pub(crate) mod tests {
380426 "refresh_token" : "test_refresh_token" ,
381427 } ) ;
382428
383- let result = build_id_token_credentials ( audience, Some ( json) ) ;
429+ let result = build_id_token_credentials ( audience, false , Some ( json) ) ;
384430 assert ! ( result. is_err( ) ) ;
385431 let err = result. unwrap_err ( ) ;
386432 assert ! ( err. is_not_supported( ) ) ;
@@ -408,7 +454,7 @@ pub(crate) mod tests {
408454 }
409455 } ) ;
410456
411- let result = build_id_token_credentials ( audience, Some ( json) ) ;
457+ let result = build_id_token_credentials ( audience, false , Some ( json) ) ;
412458 assert ! ( result. is_err( ) ) ;
413459 let err = result. unwrap_err ( ) ;
414460 assert ! ( err. is_not_supported( ) ) ;
@@ -424,11 +470,72 @@ pub(crate) mod tests {
424470 "type" : "unknown_credential_type" ,
425471 } ) ;
426472
427- let result = build_id_token_credentials ( audience, Some ( json) ) ;
473+ let result = build_id_token_credentials ( audience, false , Some ( json) ) ;
428474 assert ! ( result. is_err( ) ) ;
429475 let err = result. unwrap_err ( ) ;
430476 assert ! ( err. is_unknown_type( ) ) ;
431477 assert ! ( err. to_string( ) . contains( "unknown_credential_type" ) ) ;
432478 Ok ( ( ) )
433479 }
480+
481+ #[ tokio:: test]
482+ #[ parallel]
483+ async fn test_build_id_token_include_email_mds ( ) -> TestResult {
484+ let audience = "test_audience" . to_string ( ) ;
485+
486+ // Test with include_email = true and no source credentials (MDS Fallback)
487+ let creds = build_id_token_credentials_internal ( audience. clone ( ) , true , None ) ?;
488+ assert ! ( matches!( creds, IDTokenBuilder :: Mds ( _) ) ) ;
489+ if let IDTokenBuilder :: Mds ( builder) = creds {
490+ assert ! ( matches!( builder. format, Some ( Format :: Full ) ) ) ;
491+ }
492+
493+ // Test with include_email = false and no source credentials (MDS Fallback)
494+ let creds = build_id_token_credentials_internal ( audience. clone ( ) , false , None ) ?;
495+ assert ! ( matches!( creds, IDTokenBuilder :: Mds ( _) ) ) ;
496+ if let IDTokenBuilder :: Mds ( builder) = creds {
497+ assert ! ( matches!( builder. format, Some ( Format :: Standard ) ) ) ;
498+ }
499+
500+ Ok ( ( ) )
501+ }
502+
503+ #[ tokio:: test]
504+ #[ parallel]
505+ async fn test_build_id_token_include_email_impersonated ( ) -> TestResult {
506+ let audience = "test_audience" . to_string ( ) ;
507+ let json = json ! ( {
508+ "type" : "impersonated_service_account" ,
509+ "source_credentials" : {
510+ "type" : "service_account" ,
511+ "project_id" : "test-project" ,
512+ "private_key_id" : "test-key-id" ,
513+ "private_key" : "-----BEGIN PRIVATE KEY-----\n -----END PRIVATE KEY-----" ,
514+ "client_email" : "[email protected] " , 515+ "client_id" : "test-client-id" ,
516+ "auth_uri" : "https://accounts.google.com/o/oauth2/auth" ,
517+ "token_uri" : "https://oauth2.googleapis.com/token" ,
518+ "auth_provider_x509_cert_url" : "https://www.googleapis.com/oauth2/v1/certs" ,
519+ "client_x509_cert_url" : "https://www.googleapis.com/robot/v1/metadata/x509/source%40test-project.iam.gserviceaccount.com"
520+ } ,
521+ "service_account_impersonation_url" : "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected] :generateIdToken" 522+ } ) ;
523+
524+ // Test with include_email = true and impersonated source credentials
525+ let creds =
526+ build_id_token_credentials_internal ( audience. clone ( ) , true , Some ( json. clone ( ) ) ) ?;
527+ assert ! ( matches!( creds, IDTokenBuilder :: Impersonated ( _) ) ) ;
528+ if let IDTokenBuilder :: Impersonated ( builder) = creds {
529+ assert_eq ! ( builder. include_email, Some ( true ) ) ;
530+ }
531+
532+ // Test with include_email = false and impersonated source credentials
533+ let creds = build_id_token_credentials_internal ( audience. clone ( ) , false , Some ( json) ) ?;
534+ assert ! ( matches!( creds, IDTokenBuilder :: Impersonated ( _) ) ) ;
535+ if let IDTokenBuilder :: Impersonated ( builder) = creds {
536+ assert_eq ! ( builder. include_email, None ) ;
537+ }
538+
539+ Ok ( ( ) )
540+ }
434541}
0 commit comments