diff --git a/pg-pkg/src/middleware/auth.rs b/pg-pkg/src/middleware/auth.rs index b9248e5..f13a03b 100644 --- a/pg-pkg/src/middleware/auth.rs +++ b/pg-pkg/src/middleware/auth.rs @@ -20,6 +20,11 @@ use jsonwebtoken::{decode, errors::ErrorKind, Algorithm, DecodingKey, Validation use serde::{Deserialize, Serialize}; +/// The attribute type carrying the sender's email in the signing identity. +/// Configurable (issue #236: test environments cannot issue `pbdf.*` +/// credentials); production keeps this default. +pub(crate) const DEFAULT_EMAIL_ATTRIBUTE: &str = "pbdf.sidn-pbdf.email.email"; + #[derive(Debug, Clone)] pub(crate) struct AuthResult { pub con: Vec, @@ -411,8 +416,9 @@ enum AuthMethods { // Check the ongoing session using a token from the request. Token(String), - // Check API key using an ApiKeyStore implementation. - Key(Rc), + // Check API key using an ApiKeyStore implementation. Carries the attribute + // type used for the email in the derived signing identity. + Key(Rc, Rc), // Check the session by decoding a JWT from the request, verified against // the IRMA server's (lazily fetched) public key. @@ -461,7 +467,7 @@ where res } - AuthMethods::Key(store) => { + AuthMethods::Key(store, email_attribute) => { let auth = req.extract::().await?; let api_key = auth.token(); @@ -479,10 +485,7 @@ where // `signing_attrs.email`; an empty string signals disabled. let mut pub_attrs: Vec = Vec::new(); if !key_data.email.is_empty() { - pub_attrs.push(Attribute::new( - "pbdf.sidn-pbdf.email.email", - Some(&key_data.email), - )); + pub_attrs.push(Attribute::new(email_attribute, Some(&key_data.email))); } let mut priv_attrs: Vec = Vec::new(); @@ -644,6 +647,8 @@ pub struct Auth { method: AuthType, /// Optional API key store for Key auth method. api_key_store: Option>, + /// Attribute type carrying the email in API-key signing identities. + email_attribute: String, } impl std::fmt::Debug for Auth { @@ -665,9 +670,19 @@ impl Auth { irma_url, method, api_key_store: None, + email_attribute: DEFAULT_EMAIL_ATTRIBUTE.to_string(), } } + /// Override the attribute type used for the email in API-key signing + /// identities (issue #236). Defaults to [`DEFAULT_EMAIL_ATTRIBUTE`]; + /// test environments configure a test-scheme type here since `pbdf.*` + /// credentials cannot be issued outside production. + pub fn with_email_attribute(mut self, attribute: impl Into) -> Self { + self.email_attribute = attribute.into(); + self + } + /// Set the API key store for Key authentication. /// Use `PgApiKeyStore` for production or provide a custom implementation for testing. pub fn with_api_key_store(mut self, store: S) -> Self { @@ -695,6 +710,7 @@ where let url = self.irma_url.clone(); let auth_type = self.method.clone(); let api_key_store = self.api_key_store.clone(); + let email_attribute = self.email_attribute.clone(); async move { let auth_data = match auth_type { @@ -707,7 +723,7 @@ where let store = api_key_store.ok_or_else(|| { log::error!("API key store required for Key auth but not configured"); })?; - AuthMethods::Key(store) + AuthMethods::Key(store, Rc::from(email_attribute.as_str())) } AuthType::Token => AuthMethods::Token(url), }; diff --git a/pg-pkg/src/opts.rs b/pg-pkg/src/opts.rs index 96273e5..3421bce 100644 --- a/pg-pkg/src/opts.rs +++ b/pg-pkg/src/opts.rs @@ -61,6 +61,17 @@ pub struct ServerOpts { #[clap(short, long, env = "DATABASE_URL", value_hint = ValueHint::Url)] pub database_url: Option, + /// Attribute type carrying the email in API-key signing identities. + /// Test environments override this with a test-scheme type (e.g. + /// `irma-demo.sidn-pbdf.email.email`), since `pbdf.*` credentials cannot + /// be issued outside production (issue #236). + #[clap( + long, + env = "PKG_EMAIL_ATTRIBUTE", + default_value_t = crate::middleware::auth::DEFAULT_EMAIL_ATTRIBUTE.to_string() + )] + pub email_attribute: String, + /// IRMA server used to verify identities. #[clap(short, long, default_value = "https://is.yivi.app", value_hint = ValueHint::Url)] pub irma: String, diff --git a/pg-pkg/src/server.rs b/pg-pkg/src/server.rs index 1195619..11ce085 100644 --- a/pg-pkg/src/server.rs +++ b/pg-pkg/src/server.rs @@ -104,6 +104,7 @@ pub async fn exec(server_opts: ServerOpts) -> Result<(), PKGError> { host, port, database_url, + email_attribute, irma, irma_token, ibe_secret_path, @@ -294,7 +295,9 @@ pub async fn exec(server_opts: ServerOpts) -> Result<(), PKGError> { .guard(ApiKeyGuard) // Registered last => outermost: rate-limit before auth work. .wrap( - Auth::new(irma.clone(), AuthType::Key).with_db_pool(pool.as_ref().clone()), + Auth::new(irma.clone(), AuthType::Key) + .with_email_attribute(email_attribute.clone()) + .with_db_pool(pool.as_ref().clone()), ) .wrap(Governor::new(&sensitive_ratelimit)) .route(web::get().to(handlers::api_key_validate)), @@ -344,6 +347,7 @@ pub async fn exec(server_opts: ServerOpts) -> Result<(), PKGError> { .app_data(Data::new(ibs_sk.clone())) .wrap( Auth::new(irma.clone(), AuthType::Key) + .with_email_attribute(email_attribute.clone()) .with_db_pool(pool.as_ref().clone()), ) .wrap(Governor::new(&sensitive_ratelimit)) @@ -808,6 +812,60 @@ pub(crate) mod tests { assert!(key_response.priv_sign_key.is_none()); } + /// Issue #236: the attribute type carrying the email in an API-key signing + /// identity is configurable, so test environments (which cannot issue + /// `pbdf.*` credentials) can run the same code path under a test scheme. + #[actix_web::test] + async fn test_api_key_signing_uses_configured_email_attribute() { + let (_, _, _, _, ibs_sk) = default_setup().await; + let irma = "https://irma.example.org".to_string(); + let mock_store = MockApiKeyStore::new().with_key( + "PG-valid-key".to_string(), + default_api_key_data("sender@golden.test"), + ); + + let app = test::init_service( + App::new().service( + scope("/v2").service( + resource("/sign/key") + .app_data(Data::new(ibs_sk.clone())) + .wrap( + Auth::new(irma, AuthType::Key) + .with_email_attribute("irma-demo.sidn-pbdf.email.email") + .with_api_key_store(mock_store), + ) + .route(web::post().to(handlers::signing_key)), + ), + ), + ) + .await; + + let skr = SigningKeyRequest { + pub_sign_id: vec![], + priv_sign_id: None, + }; + let req = test::TestRequest::post() + .uri("/v2/sign/key") + .insert_header(("Authorization", "Bearer PG-valid-key")) + .set_json(&skr) + .to_request(); + + let resp = test::try_call_service(&app, req).await.unwrap(); + let key_response: SigningKeyResponse = test::try_read_body_json(resp).await.unwrap(); + + assert_eq!(key_response.status, SessionStatus::Done); + let pub_key = key_response.pub_sign_key.unwrap(); + assert_eq!(pub_key.policy.con.len(), 1); + assert_eq!( + pub_key.policy.con[0].atype, "irma-demo.sidn-pbdf.email.email", + "the configured email attribute type must be used" + ); + assert_eq!( + pub_key.policy.con[0].value.as_deref(), + Some("sender@golden.test") + ); + } + #[actix_web::test] async fn test_api_key_signing_invalid_key() { let (app, _) = setup_api_key_test_with_mock_store(