use pdk_core::logger;
use crate::api::credentials::{ClientId, ClientSecret};
use crate::api::ClientData;
use crate::implementation::hashing::hash;
use crate::implementation::model::contracts_storage::ContractsStorage;
use crate::AuthenticationError;
pub fn authenticate(
contracts_storage: &dyn ContractsStorage,
client_id: &ClientId,
client_secret: &ClientSecret,
) -> Result<ClientData, AuthenticationError> {
let Some(contracts) = contracts_storage.get_contract_by_client(client_id) else {
logger::debug!("Authentication: Client ID {client_id} does not exist for this API");
return Err(AuthenticationError::InvalidClientId);
};
if contracts.client_secret.is_empty() || contracts.client_secret_salt.is_empty() {
return Err(AuthenticationError::ContractHasNoClientSecret);
}
let hashed_secret = hash(contracts.client_secret_salt.as_str(), client_secret);
if hashed_secret == contracts.client_secret.as_bytes() {
logger::debug!(
"Authentication: Credentials for {client_id} match with a client of this API"
);
Ok(contracts.cast_to_client_information())
} else {
logger::debug!("Authentication: Client secret for {client_id} does not match.");
Err(AuthenticationError::InvalidClientSecret)
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use crate::api::authentication::authenticate;
use crate::implementation::hashing::hash;
use crate::implementation::model::contracts_storage::{
ContractsLocalStorage, ContractsStorage,
};
use crate::implementation::platform::shared::Contract;
use crate::mocks::{ManualClock, MapSharedData};
use crate::AuthenticationError;
use crate::{ClientId, ClientSecret};
const INVALID_CLIENT_ID: &str = "invalid_client_id";
const INVALID_CLIENT_SECRET: &str = "invalid_client_secret";
const VALID_CLIENT_ID: &str = "api";
const VALID_CLIENT_SECRET: &str = "gateway";
const API_ID: &str = "1234";
const SALT: &str = "someSalt";
fn invalid_client_id() -> ClientId {
ClientId::new(INVALID_CLIENT_ID.to_string())
}
fn invalid_client_secret() -> ClientSecret {
ClientSecret::new(INVALID_CLIENT_SECRET.to_string())
}
fn valid_client_id() -> ClientId {
ClientId::new(VALID_CLIENT_ID.to_string())
}
fn valid_client_secret() -> ClientSecret {
ClientSecret::new(VALID_CLIENT_SECRET.to_string())
}
fn contract(id: &str, client_id: &ClientId, client_secret: &ClientSecret) -> Contract {
let hashed_secret = hash(SALT, client_secret);
Contract {
contract_id: id.to_string(),
api_id: API_ID.to_string(),
version_id: "".to_string(),
sla_tier_id: None,
client_id: client_id.as_str().to_string(),
client_secret: String::from_utf8(hashed_secret)
.expect("Could not map secret to String"),
client_secret_salt: SALT.to_string(),
client_name: client_id.to_string(),
removed: false,
}
}
fn contract_without_secret(id: &str, client_id: &ClientId) -> Contract {
Contract {
contract_id: id.to_string(),
api_id: API_ID.to_string(),
version_id: "".to_string(),
sla_tier_id: None,
client_id: client_id.as_str().to_string(),
client_secret: "".to_string(),
client_secret_salt: "".to_string(),
client_name: client_id.to_string(),
removed: false,
}
}
#[test]
fn nonexistent_client_is_rejected() {
let clock = ManualClock::default();
let shared_data = MapSharedData::default();
let storage = ContractsLocalStorage::new(API_ID, Rc::new(clock), Rc::new(shared_data));
let validation = authenticate(&storage, &invalid_client_id(), &valid_client_secret());
assert!(validation.is_err());
let validation = authenticate(&storage, &valid_client_id(), &invalid_client_secret());
assert!(validation.is_err());
}
#[test]
#[cfg(not(fips))]
fn client_with_invalid_secret_is_rejected() {
let clock = ManualClock::default();
let shared_data = MapSharedData::default();
let storage = ContractsLocalStorage::new(API_ID, Rc::new(clock), Rc::new(shared_data));
let client_id = &valid_client_id();
let contract = contract("1", client_id, &valid_client_secret());
storage.save_contract(contract);
let validation = authenticate(&storage, client_id, &invalid_client_secret());
assert!(validation.is_err());
}
#[test]
#[cfg(not(fips))]
fn client_with_valid_id_and_secret_is_validated() {
let clock = ManualClock::default();
let shared_data = MapSharedData::default();
let storage = ContractsLocalStorage::new(API_ID, Rc::new(clock), Rc::new(shared_data));
let client_id = &valid_client_id();
let contract = contract("1", client_id, &valid_client_secret());
storage.save_contract(contract);
let validation = authenticate(&storage, client_id, &valid_client_secret());
assert!(validation.is_ok());
let client_data = validation.unwrap();
assert_eq!(client_data.client_id, VALID_CLIENT_ID);
}
#[test]
#[cfg(not(fips))]
fn removed_client_is_rejected() {
let clock = ManualClock::default();
let shared_data = MapSharedData::default();
let storage = ContractsLocalStorage::new(API_ID, Rc::new(clock), Rc::new(shared_data));
let client_id = &valid_client_id();
let contract = contract("1", client_id, &valid_client_secret());
storage.save_contract(contract);
storage.remove_contract(VALID_CLIENT_ID);
let validation = authenticate(&storage, client_id, &invalid_client_secret());
assert!(validation.is_err());
}
#[test]
fn contract_without_client_secret_rejects_with_explicit_error() {
let clock = ManualClock::default();
let shared_data = MapSharedData::default();
let storage = ContractsLocalStorage::new(API_ID, Rc::new(clock), Rc::new(shared_data));
let client_id = &valid_client_id();
let contract = contract_without_secret("1", client_id);
storage.save_contract(contract);
let result = authenticate(&storage, client_id, &valid_client_secret());
let err = result.expect_err("expected validation to fail");
assert_eq!(err, AuthenticationError::ContractHasNoClientSecret);
}
}