Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions pg-pkg/src/middleware/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Attribute>,
Expand Down Expand Up @@ -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<dyn ApiKeyStore>),
// Check API key using an ApiKeyStore implementation. Carries the attribute
// type used for the email in the derived signing identity.
Key(Rc<dyn ApiKeyStore>, Rc<str>),

// Check the session by decoding a JWT from the request, verified against
// the IRMA server's (lazily fetched) public key.
Expand Down Expand Up @@ -461,7 +467,7 @@ where

res
}
AuthMethods::Key(store) => {
AuthMethods::Key(store, email_attribute) => {
let auth = req.extract::<BearerAuth>().await?;
let api_key = auth.token();

Expand All @@ -479,10 +485,7 @@ where
// `signing_attrs.email`; an empty string signals disabled.
let mut pub_attrs: Vec<Attribute> = 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<Attribute> = Vec::new();

Expand Down Expand Up @@ -644,6 +647,8 @@ pub struct Auth {
method: AuthType,
/// Optional API key store for Key auth method.
api_key_store: Option<Rc<dyn ApiKeyStore>>,
/// Attribute type carrying the email in API-key signing identities.
email_attribute: String,
}

impl std::fmt::Debug for Auth {
Expand All @@ -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<String>) -> 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<S: ApiKeyStore + 'static>(mut self, store: S) -> Self {
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
};
Expand Down
11 changes: 11 additions & 0 deletions pg-pkg/src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ pub struct ServerOpts {
#[clap(short, long, env = "DATABASE_URL", value_hint = ValueHint::Url)]
pub database_url: Option<String>,

/// 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,
Expand Down
60 changes: 59 additions & 1 deletion pg-pkg/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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(
Expand Down
Loading