Cross-Tenant Azure API Management Authentication with Federated Credentials: A Complete Guide

Working with Azure API Management across multiple tenants? Tired of managing service principal secrets and certificates? In this blog post, I’m going to show you how to set up passwordless cross-tenant authentication using User-Assigned Managed Identities and Federated Credentials.

This approach eliminates the need for client secrets while enabling secure service-to-service authentication between API Management instances in different Entra ID tenants. Let me walk you through exactly how I implemented this pattern, from the architecture to the actual APIM policies.

The Problem: Cross-Tenant API Access

Here’s the scenario: You have an API Management instance in tenant1 (your application tenant) that needs to call protected APIs hosted in tenant2 (perhaps a partner or different business unit).

The traditional approach? Create a service principal, generate a client secret, store it securely, rotate it regularly, and hope nobody leaks it. Not ideal.

What if I told you there’s a way to do this completely passwordless using managed identities and federated credentials? Let’s look at how.

The Solution: Federated Credentials

Federated Identity Credentials allow a managed identity in one tenant to authenticate as an application in another tenant – without any secrets. Think of it as a trust relationship where tenant2 says “I trust this specific managed identity from tenant1 to act as this application.”

Here’s what makes this powerful:

  • No secrets to manage – The managed identity uses its Azure-provided certificate
  • Cross-tenant support – Works seamlessly across Entra ID tenants
  • OAuth 2.0 compliant – Uses standard client credentials flow with JWT bearer assertions
  • Role-based access – Uses app roles for granular authorisation

This is aligned with Microsoft’s latest guidance: use federated credentials over shared secrets for stronger security and reduced operational overhead.

Architecture Overview

Before we jump into the setup, let’s understand the moving parts involved:

Tenant1 (Caller/Consumer)

  • APIM Instance – Makes the API calls
  • User-Assigned Managed Identity (UAMI) – Attached to APIM for authentication
  • Multi-tenant Bridge App – App registration that enables cross-tenant auth

Tenant2 (Provider/API Owner)

  • APIM Instance – Hosts the protected APIs
  • tenant1 Bridge App Service Principal (tenant1 SP) – The caller’s identity in tenant2
  • tenant2 API App – App registration that defines the protected resource and app roles
  • tenant2 API Service Principal (tenant2 API SP) – Service principal that owns the app roles (created in Step 6)

Understanding the Two Service Principals in tenant2

This is a common point of confusion, so let me break it down:

tenant1 SP (Bridge App Service Principal)

  • Created in tenant2 via az ad sp create --id $BRIDGE_APP_ID
  • Represents who is making the request (the caller)
  • Gets assigned the access_as_app role
  • Appears in the token as the oid claim
  • Think of it as: “This is the identity of the caller in tenant2”

tenant2 API SP (API App Service Principal)

  • Created in tenant2 via az ad sp create --id $TENANT2_API_APPID
  • Represents what resource the token is for (the protected API)
  • Owns the access_as_app role definition
  • Its App ID appears in the token as the aud claim
  • Think of it as: “This is the protected resource with defined permissions”

The Role Assignment Connection

  • tenant1 SP (the caller) is granted the role → “has access_as_app role”
  • tenant2 API SP (the resource) owns the role → “defines access_as_app role”
  • This follows OAuth 2.0 pattern: audience (`aud`) must match the resource, not the authentication mechanism

The authentication flow uses a two-step token exchange:

1. tenant1 UAMI gets an assertion token from tenant1

2. Exchange that assertion for a final access token from tenant2 (with app roles)

3. tenant2 APIM validates the token and allows access

Image showing mermaid design of how Cross-Tenant Azure API Management Authentication with Federated Credentials is setup

Step-by-Step Setup

Alright, lets get into the actual setup! I’m going to walk through each step with the exact commands you need.

Prerequisites

You’ll need:

  • Owner/Contributor access to both tenant1 and tenant2
  • Azure CLI installed and logged in
  • Two APIM instances (one in each tenant)

Step 0: Set Your Variables

First, let’s set up some variables to make the commands easier. Replace these with your actual values:

# tenant1 Variables
TENANT1_ID="your-tenant1-id"
TENANT1_SUBSCRIPTION="your-tenant1-subscription"
TENANT1_APIM_NAME="tenant1-apim"
TENANT1_RG="tenant1-rg"
UAMI_NAME="tenant1-uami"

# tenant2 Variables
TENANT2_ID="your-tenant2-id"
TENANT2_SUBSCRIPTION="your-tenant2-subscription"
TENANT2_APIM_NAME="tenant2-apim"
TENANT2_API_APP_NAME="tenant2-api-app"

Step 1: Create the User-Assigned Managed Identity (tenant1)

In tenant1, create a UAMI (or use an existing one):

az login --tenant $TENANT1_ID
az account set --subscription $TENANT1_SUBSCRIPTION

# Create UAMI if it doesn't exist
az identity show -g $TENANT1_RG -n $UAMI_NAME --query clientId -o tsv >/dev/null 2>&1 || \
az identity create -g $TENANT1_RG -n $UAMI_NAME -o none

# Get UAMI details
UAMI_JSON=$(az identity show -g $TENANT1_RG -n $UAMI_NAME -o json)
UAMI_CLIENT_ID=$(echo "$UAMI_JSON" | jq -r '.clientId')
UAMI_PRINCIPAL_ID=$(echo "$UAMI_JSON" | jq -r '.principalId')
UAMI_RESOURCE_ID=$(echo "$UAMI_JSON" | jq -r '.id')

echo "UAMI Client ID: $UAMI_CLIENT_ID"
echo "UAMI Principal ID (Object ID): $UAMI_PRINCIPAL_ID"

Expected Output:

UAMI Client ID: XXXXXXXXX-XXXX-XXX-XXX-XXX-XXXXX
UAMI Principal ID (Object ID): XXXXXXXXX-XXXX-XXX-XXX-XXX-XXXXX

Step 2: Attach UAMI to tenant1 APIM

az login --tenant $TENANT1_ID
az account set --subscription $TENANT1_SUBSCRIPTION

# Get APIM resource ID
APIM_RESOURCE_ID=$(az resource show \
  -g $TENANT1_RG \
  -n $TENANT1_APIM_NAME \
  --resource-type "Microsoft.ApiManagement/service" \
  --query id -o tsv)

# Attach UAMI to APIM
az resource update --ids "$APIM_RESOURCE_ID" \
  --set identity.type="UserAssigned" \
  --set identity.userAssignedIdentities."$UAMI_RESOURCE_ID"={} -o none

echo "Attached UAMI to APIM"

Step 3: Create Multi-Tenant Bridge App (tenant1)

This app enables the cross-tenant authentication:

az login --tenant $TENANT1_ID
az account set --subscription $TENANT1_SUBSCRIPTION

# Create multi-tenant app registration
BRIDGE_APP_NAME="tenant1-bridge-app"
az ad app create \
  --display-name $BRIDGE_APP_NAME \
  --sign-in-audience AzureADMultipleOrgs

BRIDGE_APP_ID=$(az ad app list --display-name $BRIDGE_APP_NAME --query [0].appId -o tsv)
BRIDGE_APP_OBJECTID=$(az ad app list --display-name $BRIDGE_APP_NAME --query [0].id -o tsv)

echo "Bridge App ID (Client ID): $BRIDGE_APP_ID"
echo "Bridge App Object ID: $BRIDGE_APP_OBJECTID"

Important: Save both IDs – you’ll need the App ID for Step 4 and the Object ID for Step 5.

Step 4: Create Service Principal in tenant2

Now switch to tenant2 and create a service principal for the bridge app. This creates the caller’s identity in tenant2:

az login --tenant $TENANT2_ID
az account set --subscription $TENANT2_SUBSCRIPTION

# Create service principal for tenant1's bridge app in tenant2
TENANT1_SP_JSON=$(az ad sp create --id $BRIDGE_APP_ID -o json)
TENANT1_SP_OBJECTID=$(echo "$TENANT1_SP_JSON" | jq -r '.id')

echo "Created service principal in tenant2"
echo "tenant1 SP Object ID in tenant2: $TENANT1_SP_OBJECTID"

Expected output:

Created service principal in tenant2
tenant1 SP Object ID in tenant2: XXXXXXXXX-XXXX-XXX-XXX-XXX-XXXXX

What just happened? You’ve created a service principal in tenant2 that represents the tenant1 bridge app. This is the caller’s identity in tenant2.

Step 5: Configure Federated Credential (tenant1)

Back in tenant1, create the federated credential that links the UAMI to the bridge app. This is critical – the configuration must match the token claims exactly:

az login --tenant $TENANT1_ID

# Get the bridge app Object ID (not the App ID!)
BRIDGE_APP_OBJECTID=$(az ad app show --id $BRIDGE_APP_ID --query id -o tsv)

FED_CRED_NAME="APIM-UAMI-FederatedCred"

# Create federated credential using Microsoft Graph API
az rest --method POST \
  --uri "https://graph.microsoft.com/v1.0/applications/$BRIDGE_APP_OBJECTID/federatedIdentityCredentials" \
  --headers "Content-Type=application/json" \
  --body "{
    \"name\": \"$FED_CRED_NAME\",
    \"issuer\": \"https://login.microsoftonline.com/$TENANT1_ID/v2.0\",
    \"subject\": \"$UAMI_PRINCIPAL_ID\",
    \"description\": \"Allows APIM UAMI to request tokens for tenant2 API\",
    \"audiences\": [\"api://AzureADTokenExchange\"]
  }"

echo "Created federated credential"

Critical Configuration Notes:

  • issuer: Must be https://login.microsoftonline.com/{TENANT1_ID}/v2.0 (note the `/v2.0` at the end)
  • subject: Must be the UAMI Principal ID (Object ID), NOT the Client ID
  • audiences: Standard value for federated credentials is ["api://AzureADTokenExchange"]

Verify the federated credential was created:

az ad app federated-credential list --id $BRIDGE_APP_ID

You should see output like:

[
  {
    "audiences": ["api://AzureADTokenExchange"],
    "description": "Allows APIM UAMI to request tokens for tenant2 API",
    "issuer": "https://login.microsoftonline.com/{TENANT1_ID}/v2.0",
    "name": "APIM-UAMI-FederatedCred",
    "subject": "{UAMI_PRINCIPAL_ID}"
  }
]

Step 6: Create tenant2 API App with App Roles (tenant2)

In tenant2, create the API app that represents your protected resource. This is a **separate app** from the bridge app and follows OAuth 2.0 best practices:

Why a separate tenant2 API app?

  • Separation of Concerns: Authentication app (bridge) vs API resource (tenant2 API) have different purposes
  • OAuth 2.0 Compliance: Follows the resource server pattern where token aud matches the protected resource
  • Scalability: Multiple consumers can be granted different roles on the same API
  • Fine-grained RBAC: Define custom app roles specific to your API’s needs
az login --tenant $TENANT2_ID
az account set --subscription $TENANT2_SUBSCRIPTION

# Create the tenant2 API app registration
TENANT2_API_APP_NAME="tenant2-api-app"
TENANT2_API_JSON=$(az ad app create \
  --display-name $TENANT2_API_APP_NAME \
  --sign-in-audience "AzureADMyOrg" \
  -o json)
TENANT2_API_APPID=$(echo "$TENANT2_API_JSON" | jq -r '.appId')
TENANT2_API_OBJECTID=$(echo "$TENANT2_API_JSON" | jq -r '.id')

echo "Created tenant2 API app"
echo "tenant2 API App ID: $TENANT2_API_APPID"
echo "tenant2 API Object ID: $TENANT2_API_OBJECTID"

# Set identifier URI for the API
az ad app update --id $TENANT2_API_APPID --identifier-uris "api://$TENANT2_API_APPID"

# Define app role
APP_ROLE_ID=$(uuidgen)
APP_ROLE_VALUE="access_as_app"

# Add app role to tenant2 API app
az rest --method PATCH \
  --uri "https://graph.microsoft.com/v1.0/applications/$TENANT2_API_OBJECTID" \
  --headers "Content-Type=application/json" \
  --body "{
    \"appRoles\": [{
      \"allowedMemberTypes\": [\"Application\"],
      \"description\": \"Allow apps to call tenant2 API\",
      \"displayName\": \"Access tenant2 API\",
      \"id\": \"$APP_ROLE_ID\",
      \"isEnabled\": true,
      \"value\": \"$APP_ROLE_VALUE\"
    }]
  }"

echo  "Added app role to tenant2 API app"

# Create service principal for the tenant2 API app
TENANT2_API_SP_JSON=$(az ad sp create --id $TENANT2_API_APPID -o json)
TENANT2_API_SP_OBJECTID=$(echo "$TENANT2_API_SP_JSON" | jq -r '.id')

echo "Created service principal for tenant2 API app"
echo "tenant2 API SP Object ID: $TENANT2_API_SP_OBJECTID"

Important: You now have TWO service principals in tenant2:

1. tenant1 SP(from Step 4) – The caller’s identity

2. tenant2 API SP (just created) – The resource owner that defines the app roles

Note about app roles: Unlike Microsoft APIs (Graph, Azure RM), custom APIs must define their own app roles. This is by design and allows you to create permissions specific to your API’s needs.

Step 7: Assign App Role to tenant1 SP

Now comes the key authorisation step! Grant the tenant1 SP (the caller) the access_as_app role on the tenant2 API SP (the resource). This is still in tenant2:

# Still in tenant2 - assign app role to tenant1 SP
# This grants the tenant1 APIM permission to call the tenant2 API

az rest --method POST \
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$TENANT1_SP_OBJECTID/appRoleAssignments" \
  --headers "Content-Type=application/json" \
  --body "{
    \"principalId\": \"$TENANT1_SP_OBJECTID\",
    \"resourceId\": \"$TENANT2_API_SP_OBJECTID\",
    \"appRoleId\": \"$APP_ROLE_ID\"
  }"

echo "Assigned '$APP_ROLE_VALUE' role to tenant1 service principal"

What just happened?

  • principalId: tenant1 SP (the caller – “who”) – from Step 4
  • resourceId: tenant2 API SP (the resource – “what”) – from Step 6
  • appRoleId: The access_as_app role defined on tenant2 API app

This role assignment is what puts the roles: ["access_as_app"] claim in your access token!

Important Notes:

  • This step can be repeated for additional consumers with different principalId values
  • Each consumer can be granted different roles for fine-grained access control
  • The tenant2 APIM policy will validate this role claim

Awesome! You’ve now set up all the Entra ID components. Let’s move on to configuring the APIM policies.

Policies Setup Explained

This is where the magic happens. The APIM policies implement the two-step token exchange.

tenant1 APIM Policy (Inbound)

This policy performs the two-step token exchange and forwards the request to tenant2 APIM:

<policies>
    <inbound>
        <!-- Set backend URL and rewrite path -->
        <set-backend-service base-url="https://YOUR_TENANT2_APIM_NAME.azure-api.net" />
        <rewrite-uri template="/your-api-path/validate" copy-unmatched-params="true" />
        
        <!-- Step 1: Get assertion token from UAMI for the tenant1 multi-tenant app -->
        <send-request mode="new" response-variable-name="dummyResponse" timeout="20" ignore-error="true">
            <set-url>https://management.azure.com/</set-url>
            <set-method>GET</set-method>
            <authentication-managed-identity 
                resource="api://AzureADTokenExchange" 
                client-id="YOUR_UAMI_CLIENT_ID" 
                output-token-variable-name="assertionToken" />
        </send-request>
        
        <!-- Step 2: Exchange assertion token for tenant2 API token using federated credential -->
        <send-request mode="new" response-variable-name="finalTokenResponse" timeout="20" ignore-error="false">
            <set-url>https://login.microsoftonline.com/YOUR_TENANT2_ID/oauth2/v2.0/token</set-url>
            <set-method>POST</set-method>
            <set-header name="Content-Type" exists-action="override">
                <value>application/x-www-form-urlencoded</value>
            </set-header>
            <set-body>@{
                var bridgeAppId = "YOUR_BRIDGE_APP_ID";
                var tenant2ApiAppId = "YOUR_TENANT2_API_APPID";
                
                return $"grant_type=client_credentials&client_id={bridgeAppId}&scope={tenant2ApiAppId}/.default&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion={(string)context.Variables["assertionToken"]}";
            }</set-body>
        </send-request>
        
        <!-- Step 3: Extract final token -->
        <set-variable name="finalTokenBody" value="@{
            return ((IResponse)context.Variables["finalTokenResponse"]).Body.As<string>();
        }" />
        <set-variable name="finalToken" value="@{
            return JObject.Parse((string)context.Variables["finalTokenBody"])["access_token"].ToString();
        }" />
        
        <!-- Step 4: Set Authorization header with the final token -->
        <set-header name="Authorization" exists-action="override">
            <value>@("Bearer " + (string)context.Variables["finalToken"])</value>
        </set-header>
    </inbound>
    <backend>
        <forward-request />
    </backend>
    <outbound>
        <set-header name="X-Debug-Backend-URL" exists-action="override">
            <value>@(context.Request.Url.ToString())</value>
        </set-header>
        <set-header name="X-Debug-Token-Exchange" exists-action="override">
            <value>Success</value>
        </set-header>
    </outbound>
    <on-error>
        <set-header name="X-Error-Message" exists-action="override">
            <value>@(context.LastError.Message)</value>
        </set-header>
        <set-header name="X-Error-Source" exists-action="override">
            <value>@(context.LastError.Source)</value>
        </set-header>
        <set-body>@{
            var errorDetails = new JObject(
                new JProperty("error", context.LastError.Message),
                new JProperty("source", context.LastError.Source),
                new JProperty("reason", context.LastError.Reason)
            );
            
            if (context.Variables.ContainsKey("assertionToken")) {
                var token = (string)context.Variables["assertionToken"];
                errorDetails.Add("assertionToken", token.Substring(0, Math.Min(100, token.Length)) + "...");
            }
            
            if (context.Variables.ContainsKey("finalTokenResponse")) {
                var finalResp = ((IResponse)context.Variables["finalTokenResponse"]).Body.As<string>();
                errorDetails.Add("finalTokenBody", finalResp);
            }
            
            return errorDetails.ToString();
        }</set-body>
    </on-error>
</policies>

What this policy is doing

  1. set-backend-service & rewrite-uri: Configure routing first – where to send the request
  2. send-request with authentication-managed-identity: The UAMI authenticates and gets an assertion token with aud: api://AzureADTokenExchange. Note: This is wrapped in a send-request to a dummy URL (management.azure.com) just to trigger the authentication.
  3. send-request: Exchanges the assertion token for a real access token from tenant2, specifying the tenant2 API app as the scope ({tenant2ApiAppId}/.default)
  4. set-variable: Extracts the access token from the JSON response in two steps (body → token)
  5. set-header: Adds the final token as the Authorization header
  6. outbound: Debug headers to help troubleshoot
  7. on-error: Comprehensive error handling with token snippets for debugging

Key Variables to Replace

  • YOUR_UAMI_CLIENT_ID: The UAMI Client ID from Step 1
  • YOUR_TENANT2_ID: tenant2 Tenant ID
  • YOUR_BRIDGE_APP_ID: Bridge App ID from Step 3
  • YOUR_TENANT2_API_APPID: tenant2 API App ID from Step 6
  • YOUR_TENANT2_APIM_NAME: Your tenant2 APIM instance name

tenant2 APIM Policy (Inbound)

This policy validates the incoming token with role-based authorisation:

<policies>
    <inbound>
        <base />
        
        <!-- Validate JWT token with role-based authorization -->
        <validate-jwt header-name="Authorization" 
            failed-validation-httpcode="401" 
            failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
            <openid-config url="https://login.microsoftonline.com/YOUR_TENANT2_ID/.well-known/openid-configuration" />
            <audiences>
                <audience>YOUR_TENANT2_API_APPID</audience>
            </audiences>
            <issuers>
                <issuer>https://sts.windows.net/YOUR_TENANT2_ID/</issuer>
            </issuers>
            <required-claims>
                <claim name="aud" match="any">
                    <value>YOUR_TENANT2_API_APPID</value>
                </claim>
                <claim name="roles" match="any">
                    <value>access_as_app</value>
                </claim>
                <claim name="appidacr" match="any">
                    <value>2</value>
                </claim>
            </required-claims>
        </validate-jwt>
        
        <!-- Optional: Return success response without backend for testing -->
        <return-response>
            <set-status code="200" reason="OK" />
            <set-header name="Content-Type" exists-action="override">
                <value>application/json</value>
            </set-header>
            <set-body>@{
                var jwt = context.Request.Headers.GetValueOrDefault("Authorization","").AsJwt();
                var roles = jwt?.Claims.GetValueOrDefault("roles", "[]");
                var oid = jwt?.Claims.GetValueOrDefault("oid", "unknown");
                var aud = jwt?.Claims.GetValueOrDefault("aud", "unknown");
                
                return new JObject(
                    new JProperty("message", "Token validated successfully!"),
                    new JProperty("from", "tenant2 APIM"),
                    new JProperty("authenticated", true),
                    new JProperty("authorized", true),
                    new JProperty("audience", aud),
                    new JProperty("roles", roles),
                    new JProperty("callerObjectId", oid)
                ).ToString();
            }</set-body>
        </return-response>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

Variables to replace:

  • YOUR_TENANT2_ID: tenant2 Tenant ID
  • YOUR_TENANT2_API_APPID: tenant2 API App ID from Step 6

Whats being validated?

  1. openid-config: OIDC metadata endpoint for token validation (gets public keys)
  2. audiences: Token must be intended for the tenant2 API app
  3. issuers: Token must be issued by tenant2
  4. aud claim: Must match tenant2 API App ID (OAuth 2.0: audience = resource)
  5. roles claim: Caller must have the access_as_app role (authorisation)
  6. appidacr claim: Must be 2 (ensures federated credential authentication, not password/secret)

If any of these checks fail, the request is rejected with a 401.

Three-Layer Security

  • Authentication (Who are you?): Validates JWT signature, issuer, and expiration
  • Authorisation (What can you do?): Validates aud, roles, and appidacr claims
  • OAuth 2.0 Best Practice: Token audience matches the protected resource (tenant2 API app), NOT the authentication bridge app

Optional return-response section: This returns a test response without calling a backend API. Remove this in production to forward to your actual backend service.

Testing the Setup

Time to test! Here’s how to verify everything works:

Test from tenant1 APIM

Make a request to your tenant1 APIM endpoint:

curl -X GET "https://$TENANT1_APIM_NAME.azure-api.net/your-api-path/validate

You should get a successful response from tenant2’s backend API!

Expected Response

If everything is configured correctly, you should get a successful response like this:

HTTP/1.1 200 OK
content-length: 148
content-type: application/json
x-debug-backend-url: https://tenant2-apim.azure-api.net/your-api-path/validate
x-debug-token-exchange: Success

{
    "message": "Token validated successfully!",
    "from": "tenant2 APIM",
    "authenticated": true,
    "authorized": true,
    "audience": "XXXXX-XXXXXXX",
    "roles": ["access_as_app"],
    "callerObjectId": "XXXXX-XXXXXXX"
}

The response shows:

  1. audience: The tenant2 API App ID (token is FOR this resource)
  2. roles: The access_as_app role granted in Step 7
  3. callerObjectId: The tenant1 SP Object ID (who made the request)
  4. Debug headers: Confirm backend routing and token exchange success

Example response below from my testing setup:

HTTP response from API in APIM on tenant2 - showing successful

Security Considerations

A few important security notes:

  • No secrets to rotate: The managed identity uses Azure-managed certificates
  • Least privilege: Only grant the specific app role needed (access_as_app)
  • Token validation: Always validate aud, roles, and appidacr claims
  • Audit logs: Enable Entra ID sign-in logs to monitor authentication attempts
  • Scope to specific APIs: Each tenant2 API app should have its own app roles

Wrapping Up

Awesome! You’ve now set up passwordless cross-tenant authentication between two APIM instances using federated credentials. Here’s what we covered:

1. Architecture: UAMI → Bridge App → tenant2 API App with two service principals

2. Two-step token exchange: Assertion token → Final access token with app roles

3. APIM policies: Automated token acquisition and validation

4. Testing: How to verify the complete flow works

This pattern is particularly useful for:

  • Multi-tenant SaaS applications
  • Partner integrations
  • Internal service-to-service communication across business units
  • Any scenario where you want passwordless cross-tenant auth

Try various configurations to see what works best for your architecture – federated credentials are incredibly flexible! As always, if you have any questions or run into issues, feel free to reach out. I’d love to hear how you’re using this pattern in your environment!

Additional reading resources:

Leave a Reply

Discover more from Thomas Thornton Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading

Discover more from Thomas Thornton Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading