Session Management Scripts

Session Script Type Details

Usage Overview

Session management scripting enables you to customize how HawkScan maintains authenticated sessions throughout the scanning process. While authentication scripts handle the initial login process, session scripts manage what happens afterward—extracting session tokens, injecting authentication headers into every request, handling token expiration, and triggering re-authentication when needed.

Session scripts work in tandem with authentication mechanisms to ensure that HawkScan maintains valid, authenticated sessions throughout the entire test cycle. They are particularly valuable for applications using JWT tokens, custom authorization headers, or complex session management patterns that go beyond simple cookie-based authentication.

Key Responsibilities:

  • Extract session identifiers (tokens, cookies, headers) immediately after authentication
  • Inject authentication data into every subsequent request during scanning
  • Monitor token expiration and trigger re-authentication when necessary
  • Manage the session lifecycle and cleanup

When to Use Session Scripts vs Built-in Session Management

HawkScan provides built-in session management for common scenarios, including automatic cookie handling and basic JWT token extraction. Session scripts become necessary when you need:

Use Session Scripts When:

  • Your application uses JWT tokens in custom JSON response fields
  • You need to inject authorization headers into every request (e.g., Authorization: Bearer <token>)
  • Token expiration requires proactive re-authentication before tokens expire
  • Your application uses a combination of cookies and custom headers
  • External authentication commands return complex JSON structures requiring parsing
  • You need to access tokens from different parts of the authentication response
  • Session validation requires custom logic beyond standard patterns

Use Built-in Session Management When:

  • Your application uses standard session cookies that are automatically handled by the browser
  • Form-based authentication with cookies is sufficient
  • OAuth flows with standard token handling patterns
  • No custom header injection is required

Usage Scenarios

1. JWT Token Extraction and Header Injection

The most common use case for session scripts is extracting JWT tokens from JSON authentication responses and injecting them as Authorization headers into every request during scanning.

Example: OAuth 2.0 authentication returns JSON:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}

The session script extracts access_token, monitors expiration, and adds Authorization: Bearer <token> to every request.

Reference: jwt-session.kts example

2. External Command Authentication with Complex JSON

When using external authentication commands that return both cookies and headers in JSON format, session scripts parse the output and properly configure the session state.

Example: External command returns:

{
  "headers": [
    {"Authorization": "Bearer token123"},
    {"X-API-Key": "api_key_value"}
  ],
  "cookies": [
    {"JSESSIONID": "session_id_value"},
    {"XSRF-TOKEN": "csrf_token"}
  ]
}

The session script extracts all headers and cookies, stores them appropriately, and injects them into subsequent requests.

Reference: external-auth-session.kts example

3. Multi-Token Management (Headers + Cookies)

Some applications require both custom authentication headers and cookies to be present on every request. Session scripts can manage both simultaneously.

Example: An application requires:

  • A custom X-Auth-Token header with a token value
  • Session cookies (JSESSIONID, XSRF-TOKEN) managed by HttpState

The session script extracts both, stores the token value for header injection, and ensures cookies are properly maintained in the HttpState.

Reference: token-and-cookie.kts example

Simple Script/Template Example and Description

Consider the following basic session management template, session-template.kts:

import org.apache.log4j.LogManager
import com.stackhawk.hste.session.ScriptBasedSessionManagementMethodType

val logger = LogManager.getLogger("session-template")

/* This function is called after the authentication function to establish a session. */
fun extractWebSession(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) {
    logger.info("Extracting session data from authentication response")

    // Example: Extract data from the authentication response
    // val responseBody = sessionWrapper.httpMessage.responseBody.toString()

    // Example: Store values in the session for later use
    // sessionWrapper.session.setValue("token", extractedToken)

    logger.info("Session extraction completed")
}

// This function is called on each request to inject authentication data before sending
fun processMessageToMatchSession(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) {
    logger.debug("Processing message to match session")

    // Example: Inject authentication headers into the request
    // val token = sessionWrapper.session.getValue("token") as String
    // sessionWrapper.httpMessage.requestHeader.setHeader("Authorization", "Bearer $token")

    logger.debug("Message processing completed")
}

// Called internally when a new session is required (cleanup)
fun clearWebSessionIdentifiers(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) {
    logger.info("Clearing session identifiers")

    // Example: Clear stored session values
    // sessionWrapper.session.setValue("token", null)
    // sessionWrapper.session.httpState.clearCookies()

    logger.info("Session identifiers cleared")
}

// The required parameter names for your script
fun getRequiredParamsNames(): Array<String> {
    return emptyArray()
}

// Optional parameters if desired
fun getOptionalParamsNames(): Array<String> {
    return arrayOf("sessionCheckUrl")
}

Required Functions and Signatures

Session scripts must implement five functions to fulfill the session management contract:

1. extractWebSession - Initial Session Extraction

This is the entry point called immediately after authentication completes. Use it to extract session identifiers from the authentication response.

fun extractWebSession(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) {
    // Extract session data from authentication response
}

The SessionWrapper Object:

The sessionWrapper parameter provides access to three key objects:

  • .httpMessage (or .getHttpMessage()) - Contains the final HttpMessage from authentication, including:
    • responseBody - The authentication response (often JSON)
    • responseHeader - Response headers (may contain Set-Cookie directives)
    • requestingUser - User context for triggering re-authentication
  • .session (or .getSession()) - Session storage object providing:
    • setValue(String key, Any value) - Store values for later retrieval
    • getValue(String key) - Retrieve stored values
    • httpState - Cookie jar for automatic cookie management
      • addCookie(Cookie cookie) - Add cookies to the jar
      • clearCookies() - Remove all cookies
      • cookies - Array of current cookies
  • .paramValues[key] (or .getParam(String key)) - Access script configuration parameters defined in stackhawk.yml

Common Tasks in extractWebSession:

  1. Parse JSON response bodies to extract tokens
  2. Extract JWT tokens and parse claims for expiration times
  3. Store tokens in session values for later use
  4. Add cookies to the HttpState for automatic cookie handling
  5. Store header names and values for injection into requests

2. processMessageToMatchSession - Request Enhancement

This function is called before every single request during scanning. Use it to inject authentication data into outgoing requests.

fun processMessageToMatchSession(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) {
    // Inject authentication data into each request
}

The SessionWrapper in This Context:

  • .httpMessage now represents the current outbound request
    • requestHeader - Populated with default values, ready for modification
    • requestBody - Request body if applicable
    • responseHeader/responseBody - Empty (not yet sent)

Common Tasks in processMessageToMatchSession:

  1. Inject Authorization headers from stored session values
  2. Check token expiration and trigger re-authentication if needed
  3. Add custom authentication headers to requests
  4. Log authentication status for debugging
  5. Handle token refresh logic

IMPORTANT: Cookies stored in sessionWrapper.session.httpState are automatically added to requests. You do not need to manually inject cookies.

3. clearWebSessionIdentifiers - Session Cleanup

Called when a session needs to be cleared before re-authentication.

fun clearWebSessionIdentifiers(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) {
    // Clean up session state
}

Common Tasks:

  1. Clear stored session values using setValue(key, null)
  2. Clear cookies using httpState.clearCookies()
  3. Remove JWT expiration data
  4. Clear any cached authentication data

4. getRequiredParamsNames - Required Configuration

Returns an array of parameter names that must be provided in the configuration.

fun getRequiredParamsNames(): Array<String> {
    return arrayOf("jwt_token_field", "validation_url")
}

HawkScan will throw an error at startup if these parameters are not provided in the sessionScript.parameters section of stackhawk.yml.

5. getOptionalParamsNames - Optional Configuration

Returns an array of optional parameter names.

fun getOptionalParamsNames(): Array<String> {
    return arrayOf("token_type_field", "sessionCheckUrl")
}

Your script should check for the existence of optional parameters and adjust behavior accordingly.

Advanced Example: JWT Token Management with Expiration

This example demonstrates a complete JWT session management script with automatic token expiration handling:

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
import java.time.Instant
import org.apache.log4j.LogManager
import com.stackhawk.hste.session.ScriptBasedSessionManagementMethodType
import com.stackhawk.hste.extension.script.ScriptVars

val logger = LogManager.getLogger("jwt-session")
val mapper = ObjectMapper()

fun extractWebSession(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) {

    // Get configuration parameters for token extraction
    val tokenField = sessionWrapper.getParam("jwt_token_field")
    val tokenType = sessionWrapper.getParam("token_type_field") ?: "Bearer"

    logger.info("Extracting JWT token from authentication response")

    // Parse JSON response body to extract the JWT token
    val jsonObject = mapper.readValue(
        sessionWrapper.httpMessage.responseBody.bytes,
        ObjectNode::class.java
    )
    val accessToken = jsonObject.get(tokenField).asText()

    // Store the complete Authorization header value as a global variable
    // This allows other scripts (like httpsender scripts) to access it
    ScriptVars.setGlobalVar("auth_header_value", "$tokenType $accessToken")

    // Store the raw JWT in the session for later use
    sessionWrapper.session.setValue("jwt", accessToken)

    // Parse the JWT to extract claims, particularly the expiration time
    val jwt = SignedJWT.parse(accessToken)
    logger.info("JWT extracted - expires: ${jwt.jwtClaimsSet.expirationTime}")

    // Store the JWT claims for expiration checking
    sessionWrapper.session.setValue("jwt_claims", jwt.jwtClaimsSet)
}

fun processMessageToMatchSession(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) {

    // Check if the JWT token is expired or about to expire
    val nowish = Instant.now().minusMillis(15000) // 15-second buffer
    val jwtClaims = sessionWrapper.session.getValue("jwt_claims") as JWTClaimsSet?
    val isExpired = jwtClaims?.expirationTime?.toInstant()?.isBefore(nowish)

    // If token is expired, trigger re-authentication
    if (isExpired == true) {
        logger.info("JWT token expired at ${jwtClaims.expirationTime} - re-authenticating")
        synchronized(this) {
            sessionWrapper.httpMessage.requestingUser.authenticate()
        }
    }

    logger.debug("Current JWT: ${sessionWrapper.session.getValue("jwt")}")

    // Inject the Authorization header into the request
    val hdrVal = ScriptVars.getGlobalVar("auth_header_value")
    logger.debug("Adding Authorization header: $hdrVal")

    if (!hdrVal.isNullOrEmpty()) {
        sessionWrapper.httpMessage.requestHeader.setHeader("Authorization", hdrVal)
    }
}

fun clearWebSessionIdentifiers(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) {
    // Clear JWT-related session data
    sessionWrapper.session.setValue("jwt", null)
    sessionWrapper.session.setValue("jwt_claims", null)
    ScriptVars.setGlobalVar("auth_header_value", "")
}

fun getRequiredParamsNames(): Array<String> {
    return arrayOf("jwt_token_field")
}

fun getOptionalParamsNames(): Array<String> {
    return arrayOf("token_type_field")
}

Script Logic Detailed Breakdown

Imports and Setup

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode

Jackson library for JSON parsing - essential for extracting tokens from JSON responses.

import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT

Nimbus JWT library for parsing and validating JWT tokens, including claim extraction.

import com.stackhawk.hste.extension.script.ScriptVars

Utility for setting and getting global variables accessible across different script types.

Token Extraction Logic

val tokenField = sessionWrapper.getParam("jwt_token_field")

Retrieves the JSON field name from configuration (e.g., "access_token", "id_token").

val jsonObject = mapper.readValue(
    sessionWrapper.httpMessage.responseBody.bytes,
    ObjectNode::class.java
)
val accessToken = jsonObject.get(tokenField).asText()

Parses the authentication response as JSON and extracts the token using the configured field name.

ScriptVars.setGlobalVar("auth_header_value", "$tokenType $accessToken")

Stores the complete Authorization header value as a global variable. This is useful if other scripts (like httpsender scripts) need access to the token.

val jwt = SignedJWT.parse(accessToken)
sessionWrapper.session.setValue("jwt_claims", jwt.jwtClaimsSet)

Parses the JWT to extract claims, particularly the expiration time (exp claim). Storing the claims allows efficient expiration checking without repeatedly parsing the token.

Expiration Handling

val nowish = Instant.now().minusMillis(15000) // 15-second buffer

Creates a time buffer to trigger re-authentication slightly before actual expiration, preventing race conditions.

val jwtClaims = sessionWrapper.session.getValue("jwt_claims") as JWTClaimsSet?
val isExpired = jwtClaims?.expirationTime?.toInstant()?.isBefore(nowish)

Retrieves stored claims and checks if the expiration time has passed.

synchronized(this) {
    sessionWrapper.httpMessage.requestingUser.authenticate()
}

Thread safety is critical! Multiple scanning threads may detect expiration simultaneously. The synchronized block ensures only one thread triggers re-authentication.

Header Injection

sessionWrapper.httpMessage.requestHeader.setHeader("Authorization", hdrVal)

Injects the Authorization header into every request made during scanning. Without this, authenticated endpoints would return 401 Unauthorized errors.

Required HSTE Configuration

Session scripts require configuration in two places in stackhawk.yml:

1. App Configuration (if using script-based session management)

app:
  host: https://example.com
  authentication:
    usernamePassword:
      usernameField: "email"
      passwordField: "password"
      loginPath: "/api/v1/login"
      loggedInIndicator: "\\Qaccess_token\\E"
      loggedOutIndicator: "\\Q401 Unauthorized\\E"
      scanUsername: "${USERNAME}"
      scanPassword: "${PASSWORD}"

  # Link session management to the script
    sessionScript:                       # Nested under authentication (NOT app.sessionManagement)
      name: "jwt-session.kts"            # Script filename - must match hawkAddOn name
      parameters:                        # Passed via getRequiredParamsNames()/getOptionalParamsNames()
        jwt_token_field: "access_token"
        token_type_field: "Bearer"
      # NO credentials field for session scripts

2. HawkAddOn Script Configuration

hawkAddOn:
  scripts:
    - name: "jwt-session.kts"            # Must match app.authentication.sessionScript.name
      type: session
      path: "hawkscripts"                # Parent directory only (will look in hawkscripts/session/)
      language: KOTLIN

Configuration Parameters Explained:

  • name: Script filename - must match app.authentication.sessionScript.name
  • type: Must be session
  • path: Parent directory only - type subdirectory added automatically (e.g., “hawkscripts” not “hawkscripts/session”)
  • language: KOTLIN
  • Note: Session scripts do NOT have a credentials field (unlike authentication scripts)

Complete Configuration Example: External Command + Session Script

This example shows external command authentication combined with session management:

app:
  host: https://example.com
  authentication:
    external:
      type: script
      command: "python3"
      parameters: ["auth/get-token.py"]
      timeout: 30
    sessionScript:
      name: "external-auth-session.kts"\
      parameters:
        validation_url: "https://example.com/api/v1/user/profile"
        validation_regex: ".*200 OK.*"

hawkAddOn:
  scripts:
    - name: "external-auth-session.kts"
      type: session
      path: "hawkscripts"             # Parent directory only
      language: KOTLIN

How This Works:

  1. HawkScan runs python3 auth/get-token.py to authenticate
  2. The Python script outputs JSON with headers and cookies
  3. The session script (external-auth-session.kts) parses the JSON
  4. Headers and cookies are extracted and stored
  5. Every request during scanning includes these headers
  6. The validation endpoint is checked to confirm authentication success

Common Patterns and Best Practices

Pattern 1: JWT Expiration with Buffer Time

// Check expiration with a 5-minute buffer to prevent edge cases
val expirationTime = Instant.ofEpochMilli(jwtExpiresAt)
val currentTime = Instant.now()
val refreshBuffer = 300L // 5 minutes

if (currentTime.isAfter(expirationTime.minusSeconds(refreshBuffer))) {
    logger.warn("JWT token expiring soon - re-authenticating")
    synchronized(this) {
        sessionWrapper.httpMessage.requestingUser.authenticate()
    }
}

Pattern 2: Defensive Null Checking

// Always check for null when retrieving session values
val token = sessionWrapper.session.getValue("jwt")?.toString()
if (token.isNullOrEmpty()) {
    logger.error("JWT token missing from session - authentication may have failed")
    return
}
// Cookies in HttpState are automatically added to requests
// Just add them once during extractWebSession:
val cookie = Cookie(
    domain,     // e.g., "example.com"
    name,       // e.g., "JSESSIONID"
    value,      // cookie value
    "/",        // path
    null,       // expiration (null = session cookie)
    false       // secure flag
)
sessionWrapper.session.httpState.addCookie(cookie)

// No need to manually add cookies in processMessageToMatchSession!

Pattern 4: Multiple Header Injection

// Store multiple headers during extraction
val headers = mapOf(
    "Authorization" to "Bearer token123",
    "X-API-Key" to "api_key_value",
    "X-Tenant-ID" to "tenant_123"
)

headers.forEach { (name, value) ->
    sessionWrapper.session.setValue("header_$name", value)
}
sessionWrapper.session.setValue("header_names", headers.keys.joinToString(","))

// Inject all headers during processMessageToMatchSession
val headerNames = sessionWrapper.session.getValue("header_names")
    ?.toString()
    ?.split(",") ?: emptyList()

headerNames.forEach { name ->
    val value = sessionWrapper.session.getValue("header_$name")?.toString()
    if (!value.isNullOrEmpty()) {
        sessionWrapper.httpMessage.requestHeader.setHeader(name, value)
    }
}

Troubleshooting

Issue: “Session script not being called”

Symptoms: Your session script’s log messages don’t appear in hawkscan.log.

Solutions:

  1. Verify app.authentication.sessionScript.name matches the script name in hawkAddOn.scripts
  2. Check that the script path is correct relative to project root
  3. Ensure type: session is set in the hawkAddOn configuration
  4. Look for script compilation errors at HawkScan startup

Issue: “401 Unauthorized during scanning”

Symptoms: Authentication succeeds, but requests during discovery/scanning fail with 401.

Solutions:

  1. Verify processMessageToMatchSession is injecting headers correctly
  2. Add debug logging to see what headers are being added
  3. Check if token expiration is triggering too early
  4. Verify the header name and format match what the server expects
  5. Use hawk perch to test authentication and session management interactively

Issue: “JWT token expiration not being detected”

Symptoms: Scanner continues using expired tokens without re-authenticating.

Solutions:

  1. Verify JWT parsing succeeded in extractWebSession
  2. Check that jwt_claims is being stored correctly
  3. Add logging to show current time vs. expiration time
  4. Ensure the exp claim exists in your JWT
  5. Verify time zones are handled correctly (use Instant not Date)

Issue: “Cookies not being sent with requests”

Symptoms: Session cookies extracted during authentication aren’t included in subsequent requests.

Solutions:

  1. Verify cookies are being added to sessionWrapper.session.httpState, not stored as session values
  2. Check cookie domain matches the target host
  3. Ensure cookie path is / or matches request paths
  4. Verify cookies aren’t expired
  5. Check if clearWebSessionIdentifiers is being called unexpectedly

Issue: “Script parameter not found”

Symptoms: sessionWrapper.getParam("param_name") returns null.

Solutions:

  1. Verify parameter name spelling matches exactly between script and configuration
  2. Check if parameter should be in getRequiredParamsNames() or getOptionalParamsNames()
  3. Ensure parameter is defined in hawkAddOn.scripts.vars section
  4. Add null-checking and default values for optional parameters

Testing Session Scripts

Using hawk perch

# Start interactive testing mode
hawk perch --config stackhawk.yml

# In the perch prompt:
auth         # Test authentication
session      # View session details
request /api/endpoint   # Test a request with session data

Manual Testing Workflow

  1. Test Authentication First:
    hawk validate auth --config stackhawk.yml
    
  2. Add Debug Logging:
    logger.info("Extracted token: ${accessToken.take(20)}...")
    logger.info("Session values: ${sessionWrapper.session.getValue("jwt")}")
    logger.debug("Request headers: ${sessionWrapper.httpMessage.requestHeader}")
    
  3. Run a Limited Scan:
    hawk scan --config stackhawk.yml --spider-max 5
    
  4. Review hawkscan.log:
    tail -f hawkscan.log | grep -i "jwt-session"
    

Key Objects Reference

For detailed information on classes and methods used in session scripts, see Key Objects Documentation:

  • ScriptBasedSessionManagementMethodType.SessionWrapper - Main session management interface
  • HttpMessage - HTTP request/response encapsulation
  • HttpRequestHeader / HttpResponseHeader - Header manipulation
  • HttpState - Cookie jar for automatic cookie management
  • ScriptVars - Global variable storage across script types

Additional Resources