Ever tried to figure out which version of a script is running in production? Or spent 20 minutes recreating a script you accidentally overwrote in the admin console?

Yeah, me too.

ForgeRock’s browser-based script editor is fine for quick edits, but it falls apart when you need version history, code review, or deployments across multiple environments. That’s where Frodo CLI comes in—it lets you treat scripts like actual code.

The Script Management Problem

ForgeRock AM scripts power critical authentication logic:

Script Type Use Case
Scripted Decision Custom authentication logic in journeys
OAuth2 Access Token Modification Custom claims in tokens
OIDC Claims Customizing OpenID Connect claims
Social Identity Provider Profile Transform social provider data
Policy Condition Custom authorization conditions
Configuration Provider Dynamic configuration scripts

Without proper management, scripts become:

  • Scattered across environments with no single source of truth
  • Unversioned with no history of changes
  • Error-prone when manually copying between tenants

Frodo Script Commands

List All Scripts

# List scripts in a tenant
frodo script list -h https://openam-dev.forgeblocks.com/am

# Output:
# ┌─────────────────────────────────────────┬──────────────────────────────┬───────────┐
# │ Name                                     │ Type                         │ Language  │
# ├─────────────────────────────────────────┼──────────────────────────────┼───────────┤
# │ AUTHENTICATION_TREE_DECISION            │ Scripted Decision            │ JAVASCRIPT│
# │ CustomRiskEvaluation                    │ Scripted Decision            │ JAVASCRIPT│
# │ OAuth2AccessTokenModification           │ Access Token Modification    │ GROOVY    │
# └─────────────────────────────────────────┴──────────────────────────────┴───────────┘

Export Scripts

# Export all scripts to a directory
frodo script export -a -D ./scripts -h https://openam-dev.forgeblocks.com/am

# Export a specific script by name
frodo script export -n "CustomRiskEvaluation" -h https://openam-dev.forgeblocks.com/am

# Export by script ID
frodo script export -i "12345678-1234-1234-1234-123456789012" -h https://openam-dev.forgeblocks.com/am

# Export with metadata (includes description, context, etc.)
frodo script export -a -D ./scripts --include-meta -h https://openam-dev.forgeblocks.com/am

Import Scripts

# Import all scripts from a directory
frodo script import -a -D ./scripts -h https://openam-prod.forgeblocks.com/am

# Import a specific script file
frodo script import -f ./scripts/CustomRiskEvaluation.script.json -h https://openam-prod.forgeblocks.com/am

# Import and overwrite existing scripts
frodo script import -a -D ./scripts --force -h https://openam-prod.forgeblocks.com/am

Script File Format

Exported scripts are JSON files with this structure:

{
  "script": {
    "12345678-1234-1234-1234-123456789012": {
      "_id": "12345678-1234-1234-1234-123456789012",
      "name": "CustomRiskEvaluation",
      "description": "Evaluates risk based on user behavior",
      "script": "dmFyIHJpc2tTY29yZSA9IDA7...",
      "language": "JAVASCRIPT",
      "context": "AUTHENTICATION_TREE_DECISION",
      "createdBy": "id=admin,ou=user,dc=openam,dc=forgerock,dc=org",
      "creationDate": 1702000000000,
      "lastModifiedBy": "id=admin,ou=user,dc=openam,dc=forgerock,dc=org",
      "lastModifiedDate": 1702000000000
    }
  }
}

Note: The script field is Base64-encoded. Frodo handles encoding/decoding automatically.


Version Control Workflow

Repository Structure

forgerock-scripts/
├── .gitignore
├── README.md
├── scripts/
│   ├── authentication/
│   │   ├── CustomRiskEvaluation.script.json
│   │   ├── DeviceFingerprintValidation.script.json
│   │   └── MFAStepUp.script.json
│   ├── oauth/
│   │   ├── AccessTokenModification.script.json
│   │   └── CustomClaims.script.json
│   └── policy/
│       └── GeolocationCondition.script.json
└── scripts-decoded/
    ├── CustomRiskEvaluation.js
    ├── DeviceFingerprintValidation.js
    └── MFAStepUp.js

Decoding Scripts for Review

Since scripts are Base64-encoded in the JSON, create decoded versions for easier review:

#!/bin/bash
# decode-scripts.sh - Extract script content for review

mkdir -p scripts-decoded

for file in scripts/**/*.script.json; do
    name=$(basename "$file" .script.json)
    # Extract and decode the script content
    jq -r '.script | to_entries | .[0].value.script' "$file" | base64 -d > "scripts-decoded/${name}.js"
    echo "Decoded: $name"
done

Pre-commit Hook for Script Validation

#!/bin/bash
# .git/hooks/pre-commit

echo "Validating ForgeRock scripts..."

for file in scripts/**/*.script.json; do
    # Validate JSON syntax
    if ! jq empty "$file" 2>/dev/null; then
        echo "Invalid JSON: $file"
        exit 1
    fi

    # Check script is properly encoded
    script_content=$(jq -r '.script | to_entries | .[0].value.script // empty' "$file")
    if [ -z "$script_content" ]; then
        echo "Missing script content: $file"
        exit 1
    fi
done

echo "All scripts valid!"

Automated Script Deployment

GitHub Actions Workflow

# .github/workflows/deploy-scripts.yml
name: Deploy AM Scripts

on:
  push:
    branches: [main]
    paths:
      - 'scripts/**'
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ github.event.inputs.environment || 'staging' }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Install Frodo CLI
        run: npm install -g @rockcarver/frodo-cli

      - name: Validate Scripts
        run: |
          echo "Validating script files..."
          for file in scripts/**/*.script.json; do
            jq empty "$file" || exit 1
          done
          echo "All scripts valid!"

      - name: Deploy Scripts
        env:
          FRODO_HOST: ${{ secrets.FRODO_HOST }}
          FRODO_USER: ${{ secrets.FRODO_USER }}
          FRODO_PASSWORD: ${{ secrets.FRODO_PASSWORD }}
        run: |
          echo "Deploying scripts to ${{ github.event.inputs.environment || 'staging' }}..."
          frodo script import -a -D ./scripts

      - name: Verify Deployment
        env:
          FRODO_HOST: ${{ secrets.FRODO_HOST }}
          FRODO_USER: ${{ secrets.FRODO_USER }}
          FRODO_PASSWORD: ${{ secrets.FRODO_PASSWORD }}
        run: |
          echo "Verifying deployed scripts..."
          frodo script list

Script Development Best Practices

1. Use Descriptive Names

// Bad: script1, test, new
// Good: CustomRiskEvaluationForLoginJourney, DeviceFingerprintValidator

2. Add Script Headers

Every script should start with metadata:

/**
 * Script: CustomRiskEvaluation
 * Purpose: Evaluate user risk based on device, location, and behavior
 * Journey: Login, Registration
 * Author: IAM Team
 * Last Modified: 2024-12-20
 * Dependencies: Risk API service
 */

var riskScore = 0;
// ... script logic

3. Environment-Specific Configuration

Use ESVs (Environment Secrets and Variables) for environment-specific values:

// Instead of hardcoding:
// var apiUrl = "https://risk-api-prod.example.com";

// Use ESV reference:
var apiUrl = systemEnv.getProperty("esv.risk.api.url");

// Or use shared state from journey configuration:
var apiUrl = sharedState.get("riskApiUrl");

4. Script Testing Workflow

#!/bin/bash
# test-script.sh - Test a script in dev before promoting

SCRIPT_NAME="$1"
DEV_TENANT="https://openam-dev.forgeblocks.com/am"
STAGING_TENANT="https://openam-staging.forgeblocks.com/am"

# Export from dev
echo "Exporting $SCRIPT_NAME from dev..."
frodo script export -n "$SCRIPT_NAME" -D ./test-scripts -h "$DEV_TENANT"

# Import to staging for testing
echo "Importing to staging for testing..."
frodo script import -f "./test-scripts/${SCRIPT_NAME}.script.json" -h "$STAGING_TENANT"

echo "Script promoted to staging. Test in staging environment."

Script Comparison Across Environments

Compare scripts between environments to identify drift:

#!/bin/bash
# compare-scripts.sh - Compare scripts between two tenants

DEV_TENANT="https://openam-dev.forgeblocks.com/am"
PROD_TENANT="https://openam-prod.forgeblocks.com/am"

mkdir -p compare/dev compare/prod

# Export from both environments
frodo script export -a -D ./compare/dev -h "$DEV_TENANT"
frodo script export -a -D ./compare/prod -h "$PROD_TENANT"

# Compare script content (excluding timestamps)
for dev_file in compare/dev/*.script.json; do
    name=$(basename "$dev_file")
    prod_file="compare/prod/$name"

    if [ -f "$prod_file" ]; then
        # Compare just the script content
        dev_script=$(jq -r '.script | to_entries | .[0].value.script' "$dev_file")
        prod_script=$(jq -r '.script | to_entries | .[0].value.script' "$prod_file")

        if [ "$dev_script" != "$prod_script" ]; then
            echo "DIFFERENT: $name"
        else
            echo "IDENTICAL: $name"
        fi
    else
        echo "MISSING IN PROD: $name"
    fi
done

# Check for scripts only in prod
for prod_file in compare/prod/*.script.json; do
    name=$(basename "$prod_file")
    dev_file="compare/dev/$name"

    if [ ! -f "$dev_file" ]; then
        echo "ONLY IN PROD: $name"
    fi
done

Troubleshooting

Script Import Failures

# Check script exists before import
frodo script list -h $TENANT | grep "ScriptName"

# Use verbose mode
frodo script import -f script.json -h $TENANT --verbose

# Force overwrite existing script
frodo script import -f script.json -h $TENANT --force

Encoding Issues

If scripts don’t work after import, check encoding:

# Decode and verify script content
jq -r '.script | to_entries | .[0].value.script' script.json | base64 -d

# Check for special characters
jq -r '.script | to_entries | .[0].value.script' script.json | base64 -d | file -

Permission Errors

Ensure your service account has script permissions:

  • fr:am:scripts:* - Full script access
  • fr:am:scripts:read - Read-only access

Frodo CLI Series


Final Thoughts

The shift from browser-based script editing to Git-based workflows was a game changer for our team. Here’s what made the difference:

  • Code reviews actually happen now - When scripts live in Git, PRs become natural. We’ve caught bugs in code review that would have made it to production otherwise.

  • “Who changed this?” has an answer - git blame is your friend. No more guessing who modified the MFA script last week.

  • Environment drift is visible - Running the comparison script monthly caught several cases where prod had manual changes that weren’t in our repo.

The initial export takes 10 minutes. Setting up the Git workflow takes another hour. But after that, you’ll never go back to the old way.