Skip to main content

Overview

Formal’s API supports authentication using AWS Signature Version 4 (SigV4). This enables you to authenticate using your AWS IAM credentials directly, eliminating the need to manage separate API keys.

Prerequisites

Before using AWS SigV4 authentication with Formal, ensure:
  1. Cloud integration: Your AWS account is connected to Formal via a cloud integration
  2. AWS permissions: Your IAM user or role has permission to call sts:GetCallerIdentity
  3. Machine identity: A machine identity exists in Formal with the same name as your AWS principal (IAM user or role name)
You must create a machine identity in Formal with the exact same name as your AWS principal before authentication will work. See Machine Identity Setup below.

How It Works

AWS SigV4 authentication with Formal follows this flow:
  1. Your client creates a presigned AWS STS GetCallerIdentity URL using your AWS credentials
  2. The client includes this presigned URL in the Authorization header with format: Authorization: AWS4-Presigned-URL <presigned_url>
  3. Formal verifies the signature by calling AWS STS with the presigned URL
  4. Formal extracts your AWS account ID and principal name from the STS response
  5. Formal matches your AWS account to your organization via the cloud integration
  6. Formal authorizes the request using your machine identity and authorization policies

Machine Identity Setup

Before using AWS SigV4 authentication, create a machine identity in Formal that matches your AWS principal name:
  1. Navigate to Users in the Formal console
  2. Click Create Machine Identity
  3. Use the exact name of your AWS principal:
    • For IAM user arn:aws:iam::123456789012:user/DataPipeline, use name: DataPipeline
    • For IAM role arn:aws:iam::123456789012:role/ETLService, use name: ETLService
    • For assumed role arn:aws:sts::123456789012:assumed-role/MyRole/session, use name: MyRole
The machine identity name must exactly match the AWS principal name for authentication to succeed.

Usage Examples

TypeScript/JavaScript

import { defaultProvider } from "@aws-sdk/credential-provider-node";
import { HttpRequest } from "@aws-sdk/protocol-http";
import { Sha256 } from '@aws-crypto/sha256-js';
import { SignatureV4 } from "@aws-sdk/signature-v4";
import { stringify } from "qs";

// Create a presigned STS GetCallerIdentity URL
async function createPresignedURL(): Promise<string> {
  const region = process.env.AWS_REGION || "us-east-1";

  const signer = new SignatureV4({
    credentials: defaultProvider(),
    region: region,
    service: "sts",
    sha256: Sha256,
  });

  const host = `sts.${region}.amazonaws.com`;
  const req = new HttpRequest({
    headers: { 'Content-Type': 'application/json', host },
    hostname: host,
    method: "GET",
    path: '/',
    query: {
      Action: 'GetCallerIdentity',
      Version: '2011-06-15',
    },
  });

  const signed = await signer.presign(req, { expiresIn: 60 * 10 });
  return `https://${signed.hostname}${signed.path}?${stringify(signed.query)}`;
}

// Make authenticated request to Formal API
async function callFormalAPI(endpoint: string, body: any) {
  const presignedURL = await createPresignedURL();
  const url = new URL(endpoint, 'https://api.joinformal.com');

  const response = await fetch(url.toString(), {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `AWS4-Presigned-URL ${presignedURL}`
    },
    body: JSON.stringify(body),
  });

  return response.json();
}

// Example: List resources
const resources = await callFormalAPI(
  '/core.v1.ResourceService/ListResources',
  { limit: 100 }
);

Python

import boto3
import requests
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest

# Create a presigned STS GetCallerIdentity URL
def create_presigned_url():
    session = boto3.Session()
    credentials = session.get_credentials()
    region = session.region_name or 'us-east-1'

    url = f'https://sts.{region}.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15'
    request = AWSRequest(method='GET', url=url)
    SigV4Auth(credentials, 'sts', region).add_auth(request)

    return request.url

# Make authenticated request to Formal API
def call_formal_api(endpoint, body):
    presigned_url = create_presigned_url()
    url = f'https://api.joinformal.com{endpoint}'

    response = requests.post(
        url,
        json=body,
        headers={
            'Content-Type': 'application/json',
            'Authorization': f'AWS4-Presigned-URL {presigned_url}'
        }
    )

    return response.json()

# Example: List resources
resources = call_formal_api(
    '/core.v1.ResourceService/ListResources',
    {'limit': 100}
)

Go

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
    "bytes"
    "io"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/sts"
)

// Create a presigned STS GetCallerIdentity URL
func createPresignedURL(ctx context.Context) (string, error) {
    cfg, err := config.LoadDefaultConfig(ctx)
    if err != nil {
        return "", err
    }

    stsClient := sts.NewFromConfig(cfg)
    presignClient := sts.NewPresignClient(stsClient)

    presignedReq, err := presignClient.PresignGetCallerIdentity(ctx, &sts.GetCallerIdentityInput{},
        func(opts *sts.PresignOptions) {
            opts.Expires = 10 * time.Minute
        })
    if err != nil {
        return "", err
    }

    return presignedReq.URL, nil
}

// Make authenticated request to Formal API
func callFormalAPI(ctx context.Context, endpoint string, body interface{}) ([]byte, error) {
    presignedURL, err := createPresignedURL(ctx)
    if err != nil {
        return nil, err
    }

    apiURL := "https://api.joinformal.com" + endpoint

    bodyBytes, _ := json.Marshal(body)
    req, _ := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(bodyBytes))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", fmt.Sprintf("AWS4-Presigned-URL %s", presignedURL))

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

// Example: List resources
func main() {
    ctx := context.Background()
    body := map[string]interface{}{"limit": 100}

    result, err := callFormalAPI(ctx, "/core.v1.ResourceService/ListResources", body)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(result))
}

cURL

# Step 1: Generate presigned URL using AWS CLI
PRESIGNED_URL=$(aws sts get-caller-identity --output text --query 'Account' 2>/dev/null && \
  aws sts get-caller-identity --generate-cli-skeleton | \
  aws sts get-caller-identity --cli-input-json file:///dev/stdin --endpoint-url https://sts.us-east-1.amazonaws.com)

# For a simpler approach, use a script to generate the presigned URL
# Then use it with curl:

curl -X POST https://api.joinformal.com/core.v1.ResourceService/ListResources \
  -H "Content-Type: application/json" \
  -H "Authorization: AWS4-Presigned-URL <YOUR_PRESIGNED_URL>" \
  -d '{"limit": 100}'

Authorization

Requests authenticated with AWS SigV4 use machine identities for authorization. The principal name extracted from your AWS ARN becomes your identity in Formal’s authorization system.

Principal Name Extraction

Formal extracts the principal name from your AWS ARN as follows:
  • arn:aws:iam::123456789012:user/alice → Principal name: alice
  • arn:aws:iam::123456789012:role/DataPipeline → Principal name: DataPipeline
  • arn:aws:sts::123456789012:assumed-role/ETLService/session → Principal name: ETLService

Policy-Based Access Control

Use Formal’s authorization policies to control access based on machine identities and groups:
# Example: Allow specific machine identities to list resources
package formal.app

default allow := false

allow if {
    input.request.path == "/core.v1.ResourceService/ListResources"
    input.user.name in ["DataPipeline", "ETLService"]
}
Assign machine identities to groups for easier access management. This allows you to write policies based on group membership instead of individual identities.

Security Considerations

Signature Expiration

Presigned URLs expire after 10 minutes by default. Generate a fresh URL for each API request or batch of requests within that timeframe.

Organization Isolation

Formal enforces strict organization isolation:
  • Only active cloud integrations are considered for authentication
  • Your AWS account ID must match an active cloud integration
  • All API requests are scoped to your organization
  • A matching machine identity must exist

Troubleshooting

Invalid AWS signature

Error: “Invalid AWS signature” or “Signature verification failed” Solutions:
  • Verify your AWS credentials are valid and not expired
  • Ensure the presigned URL was generated correctly
  • Check that the presigned URL hasn’t expired (10-minute limit)
  • Confirm your IAM user/role has sts:GetCallerIdentity permission

No active cloud integration

Error: “No active cloud integration found for this AWS account” Solutions:
  • Verify your AWS account is connected to Formal via a cloud integration
  • Ensure the cloud integration is marked as active
  • Confirm the AWS account ID matches your cloud integration

No machine identity found

Error: “No machine identity found with name ‘X’” Solutions:
  • Create a machine identity in Formal with the exact same name as your AWS principal
  • For IAM user arn:aws:iam::123:user/alice, create identity named alice
  • For IAM role arn:aws:iam::123:role/DataPipeline, create identity named DataPipeline
  • For assumed role arn:aws:sts::123:assumed-role/MyRole/session, create identity named MyRole

Forbidden access

Error: “Forbidden” or “Permission denied” Solutions:
  • Check your authorization policies allow the machine identity to access the requested endpoint
  • Verify the machine identity is assigned to the correct groups
  • Confirm the principal name is correctly extracted from your AWS ARN
  • Review your group-based authorization policies

Missing or invalid Authorization header

Error: “No Authorization header found” or “Authorization header must start with ‘AWS4-Presigned-URL ’” Solutions:
  • Ensure you’re including the Authorization header in your request
  • Verify the header format is exactly Authorization: AWS4-Presigned-URL <presigned_url>
  • Check that the presigned URL is properly formatted and included after the space

Additional Resources