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:
- Cloud integration: Your AWS account is connected to Formal via a cloud integration
- AWS permissions: Your IAM user or role has permission to call
sts:GetCallerIdentity
- 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:
- Your client creates a presigned AWS STS
GetCallerIdentity URL using your AWS credentials
- The client includes this presigned URL in the
Authorization header with format: Authorization: AWS4-Presigned-URL <presigned_url>
- Formal verifies the signature by calling AWS STS with the presigned URL
- Formal extracts your AWS account ID and principal name from the STS response
- Formal matches your AWS account to your organization via the cloud integration
- 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:
- Navigate to Users in the Formal console
- Click Create Machine Identity
- 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}
)
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.
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
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