Overview

This document provides a comprehensive overview of the authentication hooks available for customizing authenticators. It details each hook’s purpose, usage, available parameters, identifies various existing authenticator types, and specifies which hooks are applicable to each.

The following hooks are defined:

  1. get_secret_keys: Identifies sensitive credential fields to mask in logs. This is only needed when you are using fields not currently designated as senstive fields or have created your own credential. In most cases this is not needed.

  2. get_token_fields: Maps credential fields to token request parameters. This hook is intended to be used when using a custom credential with non-standard key names, when desiring custom validation logic, or when fields need additional processing before being used.

  3. get_api_key_fields: Maps credential fields to token request parameters. This hook is intended to be used when using a custom credential with non-standard key names, when desiring custom validation logic, or when fields need additional processing before being used.

  4. generate_cache_key: Generates a unique cache key for the authenticator instance.

  5. modify_token_session: Modifies any sesssion parameters before the token request is sent.

  6. process_response: Processes the raw token response.

  7. modify_session: Modifies the HTTP session for authenticated requests.

Hooks

get_secret_keys -> List[str]

This hook identifies which additional credential fields contain sensitive information that should be hidden from logs and debugging output. It returns a list of field names that the credential filter will mask. This is crucial for security compliance and preventing accidental exposure of secrets in application logs. In practice this might include fields like client_secret, private_key, password, or any other sensitive authentication data specific to your service or credential. It must return a List[str]. The default secret fields username, password, proxy_username, and proxy_password will be masked whether the hook is provided or not. If this hook is not provided, no additional fields will be masked but the default fields will continue to be masked as usual.

When should I use this?

This hook is intended to be used when using a custom credential or when storing secret values in nonstandard credential fields.

Available Parameters:

  • metadata: Dict[str, Any]

  • logger: logging.Logger

  • credential: silo.apps.collection.Credential

Return Type:

List[str]

List of credential field names to mask

Examples:

def custom_secret_fields(
    metadata: Dict[str, Any],
    credential: Credential
) -> List[str]:
    return [
        "oauth2ClientSecret",
        "private_key",
        "api_key"
    ]

def get_secret_keys(
    metadata: Dict[str, Any],
    credential: Credential
) -> List[str]:
    secret_fields = [
        "oauth2ClientId",
        "oauth2ClientSecret",
    ]
    if metadata["oauth_type"] == "ResourceOwner":
      secret_fields.append("oauth2ResourceOwnerUsername")
      secret_fields.append("oauth2ResourceOwnerPassword")
    return secret_fields

get_token_fields -> token_fields

This hook is responsible for mapping fields from the credentials to the specific format required by the Token Authenticator. It returns a named tuple, with the field mapping.

  • token_url (str): The endpoint to request the token from

  • token_label (str): The header name to use when sending the token (e.g. Authorization)

  • token_format (str): How to format the token (e.g. Bearer {})

  • token_key (str): The dictionary key in the formatted response that contains the token

Input Parameters:

  • metadata: Dict[str, Any]

  • logger: logging.Logger

  • credential: silo.apps.collection.Credential

Return Type: Each authentication function utilizing a field configuration hook features a uniquely named hook, reflecting the variations in configurable fields for each type. The definition of the named tuple containing the field information also varies according to the specific authentication type. Below, you will find the definitions of the hook function names and their corresponding field structures. If no hook is provided, the fields will fall back to the defaults listed.

#create_token_authenticator
get_token_fields() -> token_fields
token_fields    # NamedTuple with:
    # token_url: str (Default: credential.fields["auth_endpoint"])
    # token_label: str (Default: credential.fields["auth_header"])
    # token_format: str (Default: credential.fields["token_format"] or "{}")
    # token_key: str (Default: credential.fields["token_key"])
# create_api_key_authenticator()
get_api_key_fields() -> api_key_fields
api_key_fields    # NamedTuple with:
    # token_label: str (Default: credential.fields[auth_header"] or "Authorization")
    # prefix: str (Default: credential.fields["prefix"])
    # suffix: str (Default: credential.fields["suffix"])
    # api_key: str (Default: credential.fields["apikey"])

Example:

def custom_field_mapping(
    credential: Credential,
    logger: logging.Logger
) -> token_fields:
    return token_fields(
        token_url="<https://api.example.com/token>",
        token_label="Authorization",
        token_format="Bearer {}",
        token_key="access_token"
    )

get_api_key_fields -> api_key_fields

  • token_label (str): The header name to use when sending the API key

  • prefix (str): A string to prepend to the front of the API key

  • suffix (str): A string to append to the end of the API key

  • apikey (str): The API key itself

When should I use this? This hook is intended to be used when using a custom credential with non-standard key names, when desiring custom validation logic, or when fields need additional processing before being used.

generate_cache_key -> str

This hook generates a unique identifier for the cache key of each authenticator instance. This unique cache key is used to retrieve a cached token for the correct authenticator instance and to prevent unnecessary re-authentication. It must return a str. If this hook is not provided, a unique identifier will be generated using all the fields in the credential.

When should I use this?

This hook is intended to be used when needing to set a specific cache key to use for storing token data. It should identify your authenticator instance uniquely.

Available Parameters:

  • metadata: Dict[str, Any]

  • logger: logging.Logger

  • credential: silo.apps.collection.Credential

Return Type:

str

Unique identifier for cache key

Examples:

def get_unique_id(
    metadata: Dict[str, Any],
    credential: Credential
) -> str:
    return f"{credential.fields['client_id']}-{credential.fields['endpoint']}"

def generate_cache_key(
    metadata: Dict[str, Any],
    credential: Credential
) -> str:
    headers = credential.fields.get("headers", {})
    header_list = [(key, value) for key, value in headers.items()]
    header_list.sort()
    headers_string = f"{header_list}"
    username = credential.fields["username"]
    token_fields = metadata["token_fields"]
    key = f"{username}-{token_fields.token_url}-{headers_string}-{token_fields.token_label}-{token_fields.token_format}-{token_fields.token_key}"
    return key

def generate_gcp_cache_key(credential: Credential) -> str:
    pem_key = credential.fields["oauth2ClientSecret"].replace("\\n", "\n")
    pem_hash = hashlib.sha256(pem_key.encode()).hexdigest()
    return f"{credential.fields['oauth2ClientId']}{pem_hash}"

get_token_request_args -> Dict[str, Any]

This hook allows the user to return a Dict[str, Any] that updates the arguments passed in to the request to retrieve the token. This hook supports all the parameters mentioned in requests.Session.request. It may be needed to alter the authentication process to:

  • Format credentials in the way the authentication endpoint expects

  • Add necessary headers, parameters, or body content

  • Transform credentials into the proper format for the specific API

By default, the token request will be called with the post method if the data or json parameter is fulfilled. The url for the token request as defined either in the credential or by the get_token_fields hook, and the headers, proxies, and verify SSL settings as defined in the credential. Any fields defined by the result of this hook will overwrite the default values.

If this hook is not provided, the request will be called with all the default values listed above, along with a json body object containing the username and password defined in the credential.

When should I use this?

This hook is intended to be used when needing to modify how the request to retrieve the token is formed, such as leveraging the data parameter instead of json in the request or adding additional fields.

Available Parameters:

  • metadata: Dict[str, Any]

  • logger: logging.Logger

  • credential: silo.apps.collection.Credential

Return Type:

Dict[str, Any]

Keyword arguments for requests.Session.request()

Supported Keys (from requests library):

  • method: str

  • url: str

  • params: Dict[str, str] (query parameters)

  • data: Dict[str, Any] (form data)

  • json: Dict[str, Any] (JSON body)

  • headers: Dict[str, str]

  • cookies: Dict[str, str]

  • files: Dict[str, Any]

  • auth: Tuple[str, str] (basic auth)

  • timeout: float

  • allow_redirects: bool

  • proxies: Dict[str, str]

  • verify: bool

  • stream: bool

  • cert: str

Examples:

def get_token_request_data(credential: Credential) -> Dict[str, Any]:
    return {
        "method": "POST",
        "json": {
            "grant_type": "client_credentials",
            "client_id": credential.fields["client_id"],
            "client_secret": credential.fields["client_secret"]
        },
        "headers": {
            "Content-Type": "application/json"
        },
        "params": {
            "scope": "read write"
        }
    }

def get_token_request_args(credential: Credential) -> Dict[str, Any]:
    return({
        "json": {
            "auth": {
                "methods": ["password"],
                "password": {
                    "user": {
                        "name": credential.fields["username"],
                        "domain": {
                            "id": "default"
                        },
                        "password": credential.fields["password"]
                    }
                }
            }
        },
    })

def get_jwt_token_request_args(credential: Credential) -> Dict[str, Any]:
    issuer_claim = "iss"
    scope = "scope"
    audience_claim = "aud"
    expires_claim = "exp"
    issued_at_claim = "iat"
    current_time = int(time.time())
    payload = {
        issuer_claim: credential.fields["oauth2ClientId"],
        scope: credential.fields["oauth2ClientCredentialsScope"],
        audience_claim: credential.fields["oauth2TokenURL"],
        expires_claim: current_time + credential.fields["tokenRefreshTTL"],
        issued_at_claim: current_time,
    }
    signature = jwt.encode(
        payload,
        credential.fields["oauth2ClientSecret"].replace("\\n", "\n"),
        algorithm="RS256",
        headers={"alg": "RS256", "typ": "JWT"},
    )
    return {
        "data": {
            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
            "assertion": signature,
        },
    }

def get_oauth_token_request_args(
    metadata: Dict[str, Any],
    credential: Credential
) -> Dict[str, Any]:
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    headers.update(credential.fields.get("headers", {}))
    oauth_req_info = metadata["oauth_req_info"]
    return {
        "headers": headers,
        "data": oauth_req_info.body,
        "auth": oauth_req_info.client_info,
    }

process_response -> token_response_fields

This function processes the token response from the token_url endpoint. It’s useful when the response format is nonstandard or must be converted into text, xml, etc. It can also be useful for extracting specific data from the token response such as if the token is returned in the response headers, is found in a nested structure, or otherwise needs transformation. It must return a token_response_fields named tuple containing:

  • expires_in (int): The time in seconds that the token is valid for

  • token (str): The token to use for subsequent requests

If this hook is not provided, the token_response_fields attributes will be set by handling the default logic that leverages the credential and reads the body of the response as JSON. If a static token TTL is set, it will be used as the expires_in time. Otherwise if dynamic token refresh is selected, it will attempt to retrieve the specified expiry key from the token response. To retrieve the token, it will attempt to retrieve the credential defined token payload key from the token response.

When should I use this?

This hook is intended to be used when needing to modify where the token information is located in the response or how it is accessed. This would be useful if the token is found in the headers instead of the default location of the body, or if the expires_in time is found elsewhere from the token.

Available Parameters:

  • metadata: Dict[str, Any]

  • logger: logging.Logger

  • response: requests.Response

Return Type:

token_response_fields

Named tuple with:

  • expires_in: Optional[int] # Seconds until token expires

  • token: str # The actual token value

Examples:

def custom_response_processor(response: requests.Response) -> token_response_fields:
    data = response.json()
    return token_response_fields(
        expires_in=data.get("expires_in"),  # Can be None
        token=data["access_token"]
    )

def process_response(
    response: requests.Response,
) -> token_response_fields:
    # Expiry time is found in the headers and is in minutes
    expiry_time_minutes = response.headers["inner"]["expires_in"]
    # Convert expiry time into seconds
    expires_in = expiry_time_minutes * 60
    # Token is found in the body
    token = response.json()["access_token"]
    return token_response_fields(
        expires_in=expires_in,
        token=token,
    )

def process_open_stack_response(
    response: requests.Response
) -> token_response_fields:
    return token_response_fields(
          expires_in=None,  # No expires_in time returned. Static TTL expected to be set
          token=response.headers["X-Subject-Token"],
    )

modify_session -> None

This hook customizes how the authenticator modifies HTTP sessions for subsequent API calls after authentication is complete. It allows you to set custom headers, configure proxies, or implement specialized authentication schemes. This is useful when the target API requires non-standard authentication methods, such as custom header formats, token needing to be set in the body, multiple authentication tokens, or when you need to configure session-level settings like SSL verification or connection pooling based on the authentication context.

If this hook is not provided, the session will have its auth property set to the generated TokenAuth named tuple, which when called sets the session header token_label key to the formatted token.

When should I use this?

This hook is intended to be used when needing to modify subsequent requests that might expect the token to be set in a nonstandard location for authentication where the typical approach of setting the session auth property is not sufficient.

Available Parameters:

  • metadata: Dict[str, Any]

  • logger: logging.Logger

  • session: requests.Session

  • auth_info: TokenAuth

Return Type:

None

Modifies session in-place

Example:

def custom_modify_request(session: requests.Session, auth_info: TokenAuth) -> None:
    # Modify session headers
    session.headers["X-Auth-Token"] = auth_info.token
    session.headers["X-API-Version"] = "v2"
    # Or modify other session properties
    session.verify = False
    session.timeout = 30

def modify_session(session: requests.Session, auth_info: TokenAuth) -> None:
    session.headers["X-Auth-Token"] = auth_info.token

Hook Execution Order

Hooks are executed in a particular order as follows. It is important to understand this order since hooks are usually dependent on data provided by previous hooks. It should be noted that some of these hooks occur on every API request and some only occur during a token refresh.

  1. get_secret_keys – This hook executes on every API request

  2. get_token_fields - This hook executes on every API request

  3. get_api_key_fields - This hook executes on every API request

  4. generate_cache_key - This hook executes on every API request

  5. get_token_request_args – This hook executes ONLY when a token refresh occurs

  6. process_response - This hook executes ONLY when a token refresh occurs

  7. modify_session - This hook executes on every API request

Hook Summary

Hook Name

Hook Order

Input Paramters

Return Type

Purpose

Scope

get_secret_keys

1

Credential, Logger, Metadata

List[str] This is a list of credential fields that can be set to secret. When this hook is NOT called the following fields are considered secret. username, password, proxy_username, and proxy_password NOTE this is mainly needed when custom credentials are created.

Identify sensitive credential fields to mask in logs. This is called for every API request

All Authenticators

get_api_key_fields

2

Credential, Logger, Metadata

api_key_fields

  • token_url: The endpoint to request the token from

  • token_label: The header name to use when sending the token (e.g. Authorization)

  • token_format: How to format the token (e.g. Bearer {})

  • token_key: The dictionary key in the formatted response that contains the token

NOTE this is only needed when creating a custom credential.

API Key

get_token_fields

2

Credential, Logger, Metadata

token_fields

  • token_url: The endpoint to request the token from

  • token_label: The header name to use when sending the token (e.g. Authorization)

  • token_format: How to format the token (e.g. Bearer {})

  • token_key: The dictionary key in the formatted response that contains the token

NOTE this is only needed when creating a custom credential.

Token Auth

get_token_request_args

3

Credential, Logger, Metadata

Dictionary containing anything defined in requests.Session.request

Map credential fields to token request parameters. This is called for every API request.

Token Auth, OAuth2

generate_cache_key

4

Credential, Logger, Metadata

str

Generate a unique cache key for the authenticator instance. This is called for every API request.

All

process_response

5

Response, Refresh_info, Logger, MetaData

token_response_fields

  • expires_in -This is an integer and defines the number seconds the token is valid for.

  • token – a string containing the token

Process the raw token response. When this hook is NOT called, the following occurs:

  1. The expires_in is set from credential if static otherwise it is retrieved from the token response payload using the key identified in the credential.fields[“tokenExpiresInSelector”].

  2. The token is retrieved from the payload using the credential.fields[“token_key”] as the key. Then it will format the token according to the credential.fields[“tokenFormat”]

Token Auth, OAuth2

modify_session

6

Session, Auth_info, Logger, Metadata

None - Note session is passed by reference and it is the session that this function updates.

Modify the HTTP session for authenticated requests.

All

Input Parameter Definitions

  1. Credential – Refer to the detailed Credential Fields info.

  2. Response – Refer to requests.Response

  3. refresh_info -

    • refresh_info.type - is either “static” or “tokenAuthRefreshDynamic” as set by the credential

    • refresh_info.ttl - is an int and defines the time a token is valid for. For static this comes from the credential and for dynamic it normally comes in the response to the token request.

  4. Session – Refer to Advanced Usage - Session Objects

  5. Auth_info – This field contains information about authentication, this varies based on the type of authentication used.

  6. Logger - This can be leveraged for debugging purposes. Refer to Logging

  7. Metadata - Refer to the Metadata Fields section for details.

Parameter Validation

The system uses introspection to validate hook signatures:

  • Only parameters that exist in the hook’s function signature are passed

  • If a hook requests a parameter that doesn’t exist, an InputTypeError is raised

  • Hooks can request any subset of available parameters for their execution context