Learn how to put your digital team to work with MuleSoft for Agentforce.
Contact Us 1-800-596-4880

Using Contracts Validation Library Functions

Contracts between API instances and applications give the application access to the instance based on SLA tiers. Client applications can validate their contracts by providing credentials that consist of the client ID and client secret. The PDK Contracts Validation library provides functions to validate client credentials and implement a custom client ID enforcement policy.

Only one contract at a time can exist between an API instance and an application.

To view an example policy project that uses Flex Gateway Policy Development Kit (PDK) Contracts Validation library, see Client ID Enforcement Policy Example.

Extract Client Credentials

The Contracts Validation library is available from the pdk::contracts module.

The module provides the ContractValidator object to validate client credentials represented by the ClientId and ClientSecret objects. Validate authentication and authorization credentials with the ContractValidator::authenticate() and ContractValidator::authorize() methods:

impl ContractValidator {
   pub fn authenticate(client_id: &ClientId, client_secret: &ClientSecret) -> Result<ClientData, AuthenticationError>;
   pub fn authorize(client_id: &ClientId, client_secret: &ClientSecret) -> Result<ClientData, AuthorizationError>;
}

Both methods return a ClientData struct that holds the client ID, client name, and SLA ID fields:

pub struct ClientData {
    pub client_id: String,
    pub client_name: String,
    pub sla_id: Option<String>,
}

Validate Request Authentication and Authorization Headers

Authenticate a request by extracting the client credentials from the request and then invoke the ClientValidation::authenticate() method. The pdk::contracts module provides the basic_auth_credentials() helper function to extract Basic Authentication credentials from request headers:

pub fn basic_auth_credentials(request_headers_state: &RequestHeadersState)
    -> Result<(ClientId, ClientSecret), BasicAuthError>;

This code snippet provides a simple authentication-request filter:

async fn my_authentication_filter(
  state: RequestHeadersState,
  authentication: Authentication,
  validator: &ContractValidator,
) -> Flow<()> {

    // Extract credentials
    let (client_id, client_secret) = match basic_auth_credentials(&state) {
        Ok(credentials) => credentials,
        Err(e) => {
            logger::info!("Invalid credentials: {e}");

            // For simplicity, we are using a user-defined `unathorized_response()`
            // helper function for building responses.
            return Flow::Break(unauthorized_response("Invalid credentials", 401));
        }
    };

    // Validate authentication
    let validation = validator.authenticate(&client_id, &client_secret);

    let client_data = match validation {
        Ok(client_data) => client_data,
        Err(e) => {
            logger::info!("Invalid authentication: {e}");
            return Flow::Break(unauthorized_response("Invalid authentication", 403));
        }
    };

    // Update the current authentication
    if let Some(mut auth) = authentication.authentication() {
        auth.client_id = Some(client_data.client_id);
        auth.client_name = Some(client_data.client_name);

        authentication.set_authentication(Some(&auth));
    }

    Flow::Continue(())
}

This code snippet provides a simple authorization-request filter:

async fn my_authorization_filter(
  state: RequestHeadersState,
  validator: &ContractValidator,
) -> Flow<()> {

    // Extract client id with a user defined helper `extract_client_id()` function,
    let client_id = match extract_client_id(&state) {
        Ok(credentials) => credentials,
        Err(e) => {
            logger::info!("Invalid credentials: {e}");

            // For simplicity, we are using a user-defined `unathorized_response()`
            // helper function for building responses.
            return Flow::Break(unauthorized_response("Invalid credentials", 401));
        }
    };

    // Validate authorization
    let validation = validator.authorize(&client_id);

    if let  Err(e) = validation {
        logger::info!("Invalid authentication: {e}");
        return Flow::Break(unauthorized_response("Invalid authentication", 403));
    }

    Flow::Continue(())
}

Implement Custom Extraction for Client Credentials

Invoke the ClientId::new() and ClientSecret::new() methods to extract credentials:

impl ClientId {
   pub fn new(client_id: String) -> Self;
}

impl ClientSecret {
   pub fn new(client_secret: String) -> Self;
}

After extracting the credentials, initialize a set of the credentials:

fn initialize_credentials(raw_client_id: String, raw_client_secret) -> (ClientId, ClientSecret) {
   let client_id = ClientId::new(raw_client_id);
   let client_secret = ClientSecret::new(raw_client_secret);

   (client_id, client_secret)
}
For security, the content of ClientSecret is deleted after use, and the Debug trait doesn’t reveal the ClientSecret content.

Inject the ContractValidator Object

Inject the ContractValidator object into the #[entrypoint] function and share its reference to the request filter:

#[entrypoint]
async fn configure(launcher: Launcher, validator: ContractValidator) -> Result<(), LaunchError> {

    let filter = on_request(|state, authentication| my_authentication_filter(state, authentication, &validator));
    launcher.launch(filter).await?;

    OK(())
}

Poll the Local Contracts Database

The ContractValidator object maintains a local copy of the contracts database. You must poll the local copy periodically to maintain a valid copy of the contracts. Invoke the ContractValidator::update_contracts() method to poll the local copy:

impl ContractValidator {
    pub async fn update_contracts(&self) -> Result<(), UpdateError>;
}

If the ContractValidator::update_contracts() method returns an error that specifies a connectivity problem occurred during the contracts database polling, you must implement the retry and error handling heuristics. To do this, invoke ContractValidator::update_contracts() in a period specified by the ContractValidator::UPDATE_PERIOD constant:

async fn update_my_contracts(validator: &ContractValidator, clock: Clock) {

    // Configure a new timer
    let timer = clock.period(ContractValidator::UPDATE_PERIOD);

    loop {
       // Update result handling should be customized by the programmer
       let update_result = validator.update_contracts().await;

       // Wait for the next tick
       if !timer.next_tick().await {
           // If no more ticks are available, finish the task.
           return;
       }
    }
}

The contract database initialization period specifies when to poll the contracts database at a higher frequency than update period. The ContractValidator::INITIALIZATION_PERIOD specifies the interval required by the initial polling:

async fn initialize_my_contracts(validator: &ContractValidator, clock: Clock) {

    // Configure a new timer
    let timer = clock.period(ContractValidator::UPDATE_PERIOD);

    loop {
       // Update result handling should be customized by the programmer
       let update_result = validator.update_contracts().await;

       // Wait for the next tick
       if !timer.next_tick().await {
           // If no more ticks are available, finish the task.
           return;
       }
    }
}

This snippet shows a complete contract polling task with initialization and update polling periods:

async fn update_my_contracts(validator: &ContractValidator, clock: Clock) {
    let initialization_timer = clock.period(ContractValidator::INITIALIZATION_PERIOD);

    loop {
        if validator.update_contracts().await.is_ok() {
            logger::info!("Contracts storage initialized.");
            break;
        }

        if !initialization_timer.next_tick().await {
            logger::info!("Tick event suspended.");
            break;
        }
 }

    let update_timer = initialization_timer
        .release()
        .period(ContractValidator::UPDATE_PERIOD);

   loop {
        let _ = validator.update_contracts().await;

        if !update_timer.next_tick().await {
            logger::info!("Tick event suspended.");
            break;
        }
        logger::info!("Retrying contracts storage initialization.");
    }
}

Because the contracts database polling task must run concurrently with the Launcher::launch() task, use the join!() macro from the futures crate to join both tasks. This snippet implements a complete #[entrypoint] function with polling and launching:

#[entrypoint]
async fn configure(launcher: Launcher, clock: Clock,validator: ContractValidator) -> Result<(), LauncherError> {

    let filter = on_request(|state, authentication| my_authentication_filter(state, authentication, &validator));

    let (_, launcher_result) = join! {
        update_my_contracts(&validator, clock),
        launcher.launch(filter),
    };

    launcher_result
}