Skip to main content

Autodesk Construction Cloud and FME Flow

  • February 16, 2026
  • 0 replies
  • 15 views

jkr_wrk
Influencer
Forum|alt.badge.img+36

Because it would be nice if FME Flow could connect to Autodesk Construction Cloud without Re-authenticating every week we tried to implement Autodesk Secure Service Account in FME Flow.

https://aps.autodesk.com/blog/introducing-secure-service-accounts-ssa-now-public-beta

It was pretty complex and requires the PyJWT library installed onto FME Flow.

 

Also the current Autodesk Docs connector will not work with the new WebConnection so all calls need to be made with HTTPCallers:

  1. Folder contents:
    1. GET: https://developer.api.autodesk.com/data/v1/projects/@Value(project)/folders/@Value(folder)/contents
  2. File url in the result (GET):
    1. json["relationships"]["tip"]["links"]["related"]["href"]
  3. Last version storage url (GET):
    1. json["data"]["relationships"]["storage"]["meta"]["link"]["href"]
      1. You need rewrite this url because it must
        end with /signeds3download
  4. Download url (GET):
    1. json["url"]

 

The webconnection will store the credentials and hold the token:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<ImportExportData>

<version>1</version>

<header>APOl2rGip224E9Q99xFM7krmp/kJTAAAAgAAAAEAAAA=</header>

<webservices>
<webservice>
<authentication>
<type>TOKEN</type>
<service_name>Autodesk_ACC_SSA_Credentials</service_name>
<version>1</version>
<help_url><a href=https://aps.autodesk.com/blog/introducing-secure-service-accounts-ssa-now-public-beta&gt;Secure Service Account&lt;/a&gt;</help_url>
<description></description>
<markdown_description>- Visit https://ssa-manager.autodesk.io/ and log in using your client_id + client_secret.
- Create a Service Account and copy the serviceAccountId → use as SSA_ID in FME.
- Create a Key and copy the Base64 Private Key → RSA_KEY in FME.
- Copy the key ID (kid) → RSA_KEY_ID in FME.
- Use https://developer.api.autodesk.com as BASE_URL and set TOKEN_SCOPE as needed.</markdown_description>
<connection_description></connection_description>
<markdown_connection_description></markdown_connection_description>
<verify_ssl_certificate_default>true</verify_ssl_certificate_default>
<access_token_request_format></access_token_request_format>
<token_format></token_format>
<access_token_url></access_token_url>
<authorization_header_format>Authorization: Bearer [TOKEN]</authorization_header_format>
<token_item_name></token_item_name>
<expiry_object_key></expiry_object_key>
<expiry_time_format>
<expiry_type>0</expiry_type>
<expiry_format></expiry_format>
</expiry_time_format>
<use_bearer_token>yes</use_bearer_token>
<use_query_string></use_query_string>
<nc_gui_fields>
<nc_gui_field>
<field_name>CLIENT_ID</field_name>
<gui_line>GUI OPTIONAL TEXT CLIENT_ID CLIENT_ID</gui_line>
</nc_gui_field>
<nc_gui_field>
<field_name>CLIENT_SECRET</field_name>
<gui_line>GUI OPTIONAL PASSWORD CLIENT_SECRET CLIENT_SECRET</gui_line>
</nc_gui_field>
<nc_gui_field>
<field_name>SSA_ID</field_name>
<gui_line>GUI OPTIONAL TEXT SSA_ID SSA_ID</gui_line>
</nc_gui_field>
<nc_gui_field>
<field_name>BASE_URL</field_name>
<gui_line>GUI OPTIONAL TEXT BASE_URL BASE_URL</gui_line>
</nc_gui_field>
<nc_gui_field>
<field_name>TOKEN_SCOPE</field_name>
<gui_line>GUI OPTIONAL TEXT TOKEN_SCOPE TOKEN_SCOPE</gui_line>
</nc_gui_field>
<nc_gui_field>
<field_name>RSA_KEY_ID</field_name>
<gui_line>GUI OPTIONAL PASSWORD RSA_KEY_ID RSA_KEY_ID</gui_line>
</nc_gui_field>
<nc_gui_field>
<field_name>RSA_KEY</field_name>
<gui_line>GUI OPTIONAL PASSWORD RSA_KEY RSA_KEY</gui_line>
</nc_gui_field>
</nc_gui_fields>
<nc_header_fields>
<nc_header_key>Accept</nc_header_key>
<nc_header_val>application/json</nc_header_val>
<nc_header_key>Content-Type</nc_header_key>
<nc_header_val>application/x-www-form-urlencoded</nc_header_val>
</nc_header_fields>
</authentication>
</webservice>
</webservices>

<derivedwebservices/>

<named_connections>
<token_connection>
<keyValuesXml>
<entry>
<key>BASE_URL</key>
<value>&lt;PLACEHOLDER&gt;</value>
</entry>
<entry>
<key>CLIENT_ID</key>
<value>&lt;PLACEHOLDER&gt;</value>
</entry>
<entry>
<key>CLIENT_SECRET</key>
<value>&lt;PLACEHOLDER&gt;</value>
</entry>
<entry>
<key>RSA_KEY</key>
<value>&lt;PLACEHOLDER&gt;</value>
</entry>
<entry>
<key>RSA_KEY_ID</key>
<value>&lt;PLACEHOLDER&gt;</value>
</entry>
<entry>
<key>SSA_ID</key>
<value>&lt;PLACEHOLDER&gt;</value>
</entry>
<entry>
<key>TOKEN_SCOPE</key>
<value>&lt;PLACEHOLDER&gt;</value>
</entry>
<entry>
<key>VERIFY_SSL_CERT</key>
<value>true</value>
</entry>
</keyValuesXml>
<name>Autodesk_ACC_SSA_Credentials</name>
<ownername></ownername>
<servicename>Autodesk_ACC_SSA_Credentials</servicename>
</token_connection>
</named_connections>

</ImportExportData>

The script will update the token (Scripted Parameter):

"""
Go to https://ssa-manager.autodesk.io/
Login with your client-id / client-secret
Copy both Client-ID and Secret to the WebConnection

Create a new account with clear first and last name
Copy the serviceAccountId value from account details to FME SSA_ID WebConnection

Click on Create Key

Copy the Private Key to FME RSA_KEY macroValue
PythonCreator_Base64Encode to encode into Base64 string
Copy the kid to to RSA_KEY WebConnection

Copy the kid to to RSA_KEY_ID WebConnection

BASE URL: https://developer.api.autodesk.com/
SCOPE: data:read data:write
"""

# Scripted Parameter: Build an Autodesk SSA JWT assertion from FME parameters.
# Prints diagnostics to the FME Translation Log and RETURNS the JWT string.
# Required WebConnection Parameters:
# - RSA_KEY (your private RSA key; may include tokens like <lf>, <space>, <solidus>)
# - RSA_KEY_ID (SSA key ID, used as 'kid' in JWT header)
# - CLIENT_ID (Autodesk client_id)
# - SSA_ID (Autodesk serviceAccountId / Oxygen ID)
# - BASE_URL (SSA base URL, e.g., 'https://developer.api.autodesk.com')
# - TOKEN_SCOPE (space-separated list of scopes; default: 'data:read data:write')

import fme
import fmeobjects
from fmewebservices import FMENamedConnectionManager
from time import time
import jwt # PyJWT
from base64 import b64encode, b64decode
from urllib.parse import urlencode
from urllib.request import Request, urlopen
import json

# -------------------------------------------------------------------
# CONFIGURATION
# -------------------------------------------------------------------

# Name of the FME WebConnection where SSA settings are stored
namedConnection = "Autodesk_ACC_SSA_Credentials"


# -------------------------------------------------------------------
# 1. Load the Named Connection from FME
# -------------------------------------------------------------------
conn_manager = FMENamedConnectionManager()
connection = conn_manager.getNamedConnection(namedConnection)
if connection is None:
fmeobjects.FMELogFile().logMessageString(
f"[ERROR] Named Connection '{namedConnection}' not found.",
fmeobjects.FME_ERROR
)
return "ERROR"

# -------------------------------------------------------------------
# 2. Read parameter values from the Named Connection
# -------------------------------------------------------------------
try:
params = connection.getKeyValues()
except Exception as e:
fmeobjects.FMELogFile().logMessageString(
f"[ERROR] Could not read Named Connection: {e}",
fmeobjects.FME_ERROR
)
return "ERROR"

# Required fields:
RSA_KEY = params.get("RSA_KEY", "") # Base64-encoded RSA private key
RSA_KEY_ID = params.get("RSA_KEY_ID", "") # Key ID (kid) from Autodesk SSA
CLIENT_ID = params.get("CLIENT_ID", "") # Used for Basic Auth in token request
CLIENT_SECRET = params.get("CLIENT_SECRET", "") # Used for Basic Auth in token request
SSA_ID = params.get("SSA_ID", "") # serviceAccountId (ssaId)
BASE_URL = params.get("BASE_URL", "") # Always: https://developer.api.autodesk.com/authentication/v2/token
TOKEN_SCOPE = params.get("TOKEN_SCOPE", "") # data:read data:write

# -------------------------------------------------------------------
# 3. Validate required values
# -------------------------------------------------------------------
if not all([RSA_KEY, RSA_KEY_ID, CLIENT_ID, CLIENT_SECRET, SSA_ID, BASE_URL]):
fmeobjects.FMELogFile().logMessageString(
"[ERROR] Missing required configuration values.",
fmeobjects.FME_ERROR
)
return "ERROR"

# -------------------------------------------------------------------
# 4. Construct the Autodesk token endpoint URL
# -------------------------------------------------------------------
# Always: https://developer.api.autodesk.com/authentication/v2/token
TOKEN_URL = BASE_URL.rstrip("/") + "/authentication/v2/token"

# -------------------------------------------------------------------
# 5. Decode the Base64 RSA private key from SSA Manager
# -------------------------------------------------------------------
try:
# RSA key arrives as Base64 text → decode to PEM string
private_key = b64decode(RSA_KEY, validate=True).decode("utf-8").strip()
except Exception as e:
fmeobjects.FMELogFile().logMessageString(
f"[ERROR] Invalid Base64 RSA key: {e}",
fmeobjects.FME_ERROR
)
return "ERROR"
"""
if not private_key.startswith("-----BEGIN"):
print("[WARN] Decoded RSA key does not look like a PEM block.")
# Minimal diagnostics
print(f"[INFO] RSA_KEY length (raw): {len(private_key)}")
print(f"[INFO] RSA_KEY start: {private_key[:35]}")
print(f"[INFO] RSA_KEY_ID length (raw): {len(RSA_KEY_ID)}")
print(f"[INFO] RSA_KEY_ID start: {RSA_KEY_ID[:10]}")
"""
# -------------------------------------------------------------------
# 6. Build JWT claims (Autodesk SSA specification)
# -------------------------------------------------------------------
now = int(time())
claims = {
"iss": CLIENT_ID, # OAuth client_id
"sub": SSA_ID, # serviceAccountId (Oxygen/SSA)
"aud": TOKEN_URL, # must match the token endpoint exactly
"iat": now, # issued-at (seconds since epoch)
"exp": now + 300, # expires in 5 minutes
"scope": TOKEN_SCOPE.split(), # SSA scopes as list
}

# JWT header containing the Key ID (kid)
jwt_headers = {"kid": RSA_KEY_ID, "typ": "JWT"}


# -------------------------------------------------------------------
# 7. Sign the JWT with RS256 using the private key
# -------------------------------------------------------------------
try:
assertion = jwt.encode(
claims,
private_key,
algorithm="RS256",
headers=jwt_headers
)

# Ensure string output (older PyJWT sometimes returns bytes)
if isinstance(assertion, bytes):
assertion = assertion.decode("utf-8")

except Exception as e:
fmeobjects.FMELogFile().logMessageString(
f"[ERROR] JWT creation failed: {e}",
fmeobjects.FME_ERROR
)
return "ERROR"

# -------------------------------------------------------------------
# 8. Prepare the token request (JWT → Access Token)
# -------------------------------------------------------------------
form_data = {
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion,
"scope": TOKEN_SCOPE,
}
body_bytes = urlencode(form_data).encode("utf-8")

# Basic Auth header: base64(client_id:client_secret)
base64_cics = b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode("utf-8")).decode("ascii")
http_headers = {
"Accept": "application/json",
"Authorization": f"Basic {base64_cics}",
"Content-Type": "application/x-www-form-urlencoded",
}
# Build HTTP POST request object
req = Request(TOKEN_URL, data=body_bytes, headers=http_headers, method="POST")
# -------------------------------------------------------------------
# 9. Execute the token request and parse the response
# -------------------------------------------------------------------
try:
response = json.loads(
urlopen(req, timeout=90).read().decode("utf-8")
)

token = response.get("access_token", "")
if not token:
fmeobjects.FMELogFile().logMessageString(
f"[ERROR] Response missing access_token: {response}",
fmeobjects.FME_ERROR
)
return "ERROR"

except Exception as e:
# Attempt to extract a detailed server error message
try:
body = e.read().decode("utf-8") if hasattr(e, "read") else ""
except:
body = ""
fmeobjects.FMELogFile().logMessageString(
f"[ERROR] Token request failed: {e} {body}",
fmeobjects.FME_ERROR
)
return f"ERROR: {str(e)} {body}"

# -------------------------------------------------------------------
# 10. Store the access token back into the Named Connection
# -------------------------------------------------------------------
# This makes FME automatically inject:
# Authorization: Bearer <token>
# into all HTTPCaller requests using this connection.
connection.setAccessToken(token)
conn_manager.updateNamedConnection(connection)
# -------------------------------------------------------------------
# 11. Script completed successfully
# -------------------------------------------------------------------
return True

 

Hope this gives others some help setting up the same.