pdk-contracts-lib 1.9.1-alpha.2

PDK Contracts Library
Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

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);
    }
}