View a markdown version of this page

Agent identity for domain-joined fleets - Amazon WorkSpaces Applications

Agent identity for domain-joined fleets

On a domain-joined fleet, Windows must log the user on to the Active Directory domain, and an agent can't type a password. Instead, certificate-based authentication logs the user on with a short-lived certificate that WorkSpaces Applications issues for the session. To issue that certificate, WorkSpaces Applications needs the user's Active Directory identity, which your identity provider (IdP) supplies in a SAML assertion.

The agent therefore runs two on-behalf-of (OBO) exchanges from the same user token:

  • An OIDC exchange. The agent exchanges the resulting token for temporary AWS credentials and signs its Model Context Protocol (MCP) requests with them. This is the same exchange as for non-domain-joined fleets, and it carries the agent and user identities into CloudTrail.

  • A SAML exchange. The agent passes the resulting SAML assertion to the MCP server. WorkSpaces Applications uses it to establish the streaming session and to issue the logon certificate for the user's Active Directory account.

The steps in this topic use Microsoft Entra ID. To use another IdP, it must be able to return a SAML assertion from the OBO or token exchange, with the attributes listed in Step 2: Configure the SAML attributes.

To set up agent identity, complete the following steps:

Prerequisites

  • A stack with agent access turned on, associated with a running domain-joined fleet.

  • Certificate-based authentication turned on for the directory configuration, and working for human users. If a user can't log on with certificate-based authentication, an agent can't either. For more information, see Certificate-Based Authentication.

  • The OIDC configuration for agent identity, which you complete in Step 1: Configure your identity provider and Step 2: Configure IAM for non-domain-joined fleets. The agent uses that configuration for its AWS credentials.

  • (Optional) A CloudTrail trail that logs data events for agent access, if you want tool calls recorded. For more information, see Monitoring.

Step 1: Create a SAML application in your identity provider

Create a SAML application for AWS that the agent can request an assertion from in an OBO exchange. This application is separate from the AWS federation application that you created for the OIDC exchange.

To create the SAML application in Microsoft Entra ID
  1. Register an application with the reply URL https://signin.aws.amazon.com/saml, create its service principal with SAML as the single sign-on mode, and add urn:amazon:webservices as an identifier URI.

    If another application in your tenant already uses urn:amazon:webservices, add a fragment, such as urn:amazon:webservices#app-name. AWS ignores the fragment when it matches the audience. Use the same value as the scope prefix in the OBO exchange.

  2. Add an application-specific token signing certificate to the service principal. Entra can't issue a SAML assertion in an OBO exchange without one.

  3. Expose a scope named access_as_agent on the SAML application, and grant the agent application consent to it. This consent is separate from the consent for the scope on the AWS federation application. Without it, the SAML exchange fails even when the OIDC exchange works.

  4. Create a claims-mapping policy with the following definition. It emits the attributes in Step 2: Configure the SAML attributes. Replace the role ARN, the IAM SAML identity provider ARN, and the domain name with your own values.

    { "ClaimsMappingPolicy": { "Version": 1, "IncludeBasicClaimSet": "false", "ClaimsSchema": [ { "Source": "user", "ID": "userprincipalname", "SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" }, { "Value": "arn:aws:iam::111122223333:role/role-name,arn:aws:iam::111122223333:saml-provider/AgentAccessSAML", "SamlClaimType": "https://aws.amazon.com/SAML/Attributes/Role" }, { "Source": "user", "ID": "userprincipalname", "SamlClaimType": "https://aws.amazon.com/SAML/Attributes/RoleSessionName" }, { "Source": "user", "ID": "mailnickname", "SamlClaimType": "https://aws.amazon.com/SAML/Attributes/SourceIdentity" }, { "Source": "application", "ID": "objectid", "SamlClaimType": "https://aws.amazon.com/SAML/Attributes/PrincipalTag:AgentAccessAgentId" }, { "Source": "application", "ID": "displayname", "SamlClaimType": "https://aws.amazon.com/SAML/Attributes/PrincipalTag:AgentAccessAgentName" }, { "Source": "user", "ID": "mailnickname", "SamlClaimType": "https://aws.amazon.com/SAML/Attributes/PrincipalTag:AgentAccessUserId" }, { "Source": "user", "ID": "userprincipalname", "SamlClaimType": "https://aws.amazon.com/SAML/Attributes/PrincipalTag:UserPrincipalName" }, { "Source": "user", "ID": "onpremisesecurityidentifier", "SamlClaimType": "https://aws.amazon.com/SAML/Attributes/PrincipalTag:ObjectSid" }, { "Value": "corp.example.com", "SamlClaimType": "https://aws.amazon.com/SAML/Attributes/PrincipalTag:Domain" } ] } }

    The Domain entry is optional. Include it only when the UPN suffix differs from the domain name.

  5. Create the policy and assign it to the service principal of the SAML application with the Microsoft Graph API. You can't create or assign a claims-mapping policy in the Microsoft Entra admin center. For example, with the Azure CLI:

    # Create the policy. In the request body, pass the definition as a single JSON string: # { "displayName": "AgentAccessSAMLClaims", "definition": [ "policy-definition" ] } az rest --method POST \ --url https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies \ --headers Content-Type=application/json \ --body @claims-policy-request.json # Assign the policy to the service principal of the SAML application az rest --method POST \ --url 'https://graph.microsoft.com/v1.0/servicePrincipals/saml-sp-object-id/claimsMappingPolicies/$ref' \ --headers Content-Type=application/json \ --body '{"@odata.id": "https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/policy-id"}'
  6. Set acceptMappedClaims to true on the SAML application. Without it, Entra rejects the token request.

    az rest --method PATCH \ --url https://graph.microsoft.com/v1.0/applications/saml-app-object-id \ --headers Content-Type=application/json \ --body '{"api": {"acceptMappedClaims": true}}'

For more information about SAML federation with WorkSpaces Applications, see Setting Up SAML.

Step 2: Configure the SAML attributes

The SAML assertion must carry the agent and user identities, and the Active Directory attributes that WorkSpaces Applications uses to build the logon certificate. Except for the name ID, each attribute name starts with https://aws.amazon.com/SAML/Attributes/.

SAML attributes for agent identity on domain-joined fleets
Attribute Value
Name ID The user's UPN, in the format user@ad-dns-name. Source it from the user's userprincipalname attribute. Entra ignores a constant value for the name ID. WorkSpaces Applications writes the name ID into the subject alternative name of the logon certificate.
Role The role ARN and the IAM SAML identity provider ARN, separated by a comma
RoleSessionName The user's UPN
SourceIdentity The user's short name
PrincipalTag:AgentAccessAgentId The agent's immutable object ID
PrincipalTag:AgentAccessAgentName (Optional) The agent's display name
PrincipalTag:AgentAccessUserId The user's short name
PrincipalTag:UserPrincipalName The user's UPN
PrincipalTag:ObjectSid The security identifier (SID) of the Active Directory user, such as S-1-5-21-.... WorkSpaces Applications adds it to the certificate as the strong certificate mapping that domain controllers require.
PrincipalTag:Domain (Optional) The fully qualified domain name of the Active Directory domain. Use it when the UPN suffix differs from the domain name.

Keep the following in mind when you configure the attributes:

  • In Microsoft Entra ID, set IncludeBasicClaimSet to false in the claims-mapping policy. Additional claims cause AWS Sign-In to show an interactive role selection page, which an agent can't complete.

  • Use the same values for SourceIdentity and PrincipalTag:AgentAccessUserId as in the OIDC exchange, such as the user's mailnickname for the short name. The streaming session takes the user from the SAML assertion, and the MCP events in CloudTrail take it from the OIDC token. If the values differ, the two name different users.

  • For users synchronized from Active Directory, source PrincipalTag:ObjectSid from onpremisesecurityidentifier. For other users, that attribute is empty, and the logon certificate can't be mapped to the account.

    Important

    For testing only, you can emit the SID of an Active Directory account as a constant value. A constant value in a claims-mapping policy applies to every user of the application, so every user would get the same SID. Use a constant only for a single test user, or with a separate SAML application and policy for each user. Don't use a constant SID in production. To read the SID, run Get-ADUser -Identity user-name -Properties objectSid on a domain-joined computer.

  • You don't need TransitiveTagKeys, because this flow doesn't chain roles. If you include it, emit one attribute value for each tag key. A single comma-separated value isn't a valid tag key, and the role assumption fails.

Step 3: Configure IAM for SAML

To configure IAM for the SAML exchange
  1. Download the federation metadata of the SAML application from your IdP, and create an IAM SAML identity provider from it.

    aws iam create-saml-provider --name AgentAccessSAML \ --saml-metadata-document file://saml-metadata.xml
  2. Add the following statements to the trust policy of the role that you created for the OIDC exchange. The role then trusts both identity providers. As in the OIDC trust policy, the Null conditions are in a separate statement from sts:SetSourceIdentity.

    { "Sid": "AssumeRoleWithSAMLBothIdentities", "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::111122223333:saml-provider/AgentAccessSAML" }, "Action": [ "sts:AssumeRoleWithSAML", "sts:TagSession" ], "Condition": { "StringEquals": { "SAML:aud": "https://signin.aws.amazon.com/saml" }, "Null": { "aws:RequestTag/AgentAccessAgentId": "false", "aws:RequestTag/AgentAccessUserId": "false" } } }, { "Sid": "SAMLSetSourceIdentity", "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::111122223333:saml-provider/AgentAccessSAML" }, "Action": "sts:SetSourceIdentity", "Condition": { "StringEquals": { "SAML:aud": "https://signin.aws.amazon.com/saml" } } }
  3. Allow the role to stream from the stack. Without this permission, the streaming session can't be established.

    { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "appstream:Stream", "Resource": "arn:aws:appstream:region:111122223333:stack/stack-name" } ] }

Step 4: Connect the agent

The agent runs both exchanges from the same user token. It signs MCP requests with the credentials from the OIDC exchange, as shown in Step 3: Connect the agent, and passes the SAML assertion and the stack ARN in the _meta field of every request. SAML assertions are too large for HTTP headers.

Microsoft Entra ID returns the assertion from the OBO exchange as a bare SAML assertion, encoded in base64url. AWS Sign-In expects a SAML response, so the agent wraps the assertion in an unsigned samlp:Response element. AWS Sign-In validates the signature of the assertion inside it.

import base64 import json import uuid from datetime import datetime, timezone import requests from botocore.auth import SigV4Auth from botocore.awsrequest import AWSRequest from botocore.credentials import Credentials REGION = "region" MCP_URL = f"https://agentaccess-mcp.{REGION}.api.aws/mcp" # 1. SAML on-behalf-of exchange, from the same user token as the OIDC exchange obo_saml = requests.post( "https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token", data={ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "client_id": "agent-client-id", "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion": client_assertion(), "assertion": user_token, "requested_token_use": "on_behalf_of", "requested_token_type": "urn:ietf:params:oauth:token-type:saml2", "scope": "urn:amazon:webservices/.default", }, ).json() # 2. Decode the base64url assertion and wrap it in a SAML response raw = obo_saml["access_token"] assertion_xml = base64.urlsafe_b64decode(raw + "=" * (-len(raw) % 4)).decode() now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") saml_response = base64.b64encode(( '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ' f'ID="_{uuid.uuid4()}" Version="2.0" IssueInstant="{now}" ' 'Destination="https://signin.aws.amazon.com/saml">' '<samlp:Status><samlp:StatusCode ' 'Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>' f'{assertion_xml}</samlp:Response>' ).encode()).decode() # 3. Send signed MCP requests with the OIDC credentials (creds), # with the SAML assertion and the stack ARN in _meta credentials = Credentials( creds["AccessKeyId"], creds["SecretAccessKey"], creds["SessionToken"] ) auth_meta = { "aws.agentaccess/workspacesApplicationsSamlAssertion": saml_response, "aws.agentaccess/workspacesApplicationsStackArn": "arn:aws:appstream:region:111122223333:stack/stack-name", } session_id = None def mcp_request(method, params=None, request_id=1): """Send one JSON-RPC request. _meta is added before the body is signed.""" params = dict(params or {}) params["_meta"] = auth_meta body = json.dumps( {"jsonrpc": "2.0", "id": request_id, "method": method, "params": params} ).encode() headers = {"Content-Type": "application/json"} if session_id: headers["Mcp-Session-Id"] = session_id request = AWSRequest(method="POST", url=MCP_URL, data=body, headers=headers) SigV4Auth(credentials, "agentaccess-mcp", REGION).add_auth(request) # Send exactly the bytes that were signed response = requests.post(MCP_URL, data=body, headers=dict(request.headers)) response.raise_for_status() return response init = mcp_request("initialize", { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "my-agent", "version": "1.0"}, }) session_id = init.headers["Mcp-Session-Id"] tools = mcp_request("tools/list", request_id=2).json()["result"]["tools"] screenshot = mcp_request( "tools/call", {"name": "agentaccess___screenshot", "arguments": {}}, request_id=3 ).json()

The desktop can take a few moments to connect after initialize. If the first tools/list or tool call fails, wait and retry. To end the session, send a signed DELETE request to the endpoint with the Mcp-Session-Id header.

The example follows these rules. Follow them in your own client too:

  • Include _meta on every request, not only on initialize. If it's missing from a later request, the streaming session is lost.

  • Compute the SigV4 signature over the exact bytes that you send, including _meta. If you change the body after you sign it, the request fails with a signature mismatch or a Content-Length error.

  • The initialize response returns an Mcp-Session-Id header. Send it on every later request. Without it, the service returns HTTP 400 with the message Missing Mcp-Session-Id header.

  • Send the SAML assertion and the stack ARN only in _meta. The service rejects a request that also sends them as headers.

The stack ARN must be in the same AWS account as the credentials that sign the request.

The SAML assertion is valid for a short time. When the agent starts a new session, it runs both exchanges again. Both exchanges require a valid token for the user, so the agent must refresh the user's token when it expires, as described in Step 3: Connect the agent for non-domain-joined fleets.

The example signs every request with the same static credentials, so requests fail after the role session expires. Before that happens, run both exchanges again and sign later requests with the new credentials and SAML assertion.

Step 5: Verify agent identity

The following signals confirm that agent identity works on a domain-joined fleet. Each one proves something different.

  1. Both identities reached AWS. In CloudTrail, the AssumeRoleWithSAML event for the role contains AgentAccessAgentId, AgentAccessUserId, UserPrincipalName, and ObjectSid in principalTags, and the user in sourceIdentity. The ObjectSid tag shows that the security identifier for the logon certificate arrived. The event can take several minutes to appear in CloudTrail.

  2. The user is recorded on agent actions. Events with the event source agentaccess-mcp.amazonaws.com show the user in userIdentity.sessionContext.sourceIdentity. These events belong to the OIDC role session, because the agent signs MCP requests with the OIDC credentials. The SAML assertion establishes the streaming session but doesn't sign AWS requests.

    Note

    Agent actions (tool calls, recorded as CallReadOnlyTool or CallPrivilegedTool) are data events. They appear in CloudTrail only if your trail logs data events for agent access. Without data events, you see only management events, such as Initialize, and not the agent's tool calls. For more information, see Monitoring.

  3. Windows logged on with the certificate. On the domain controller, the Security log shows event 4768 with pre-authentication type 16 and a certificate thumbprint, followed by events 4769 and 4624. Event 4771 indicates a failed certificate logon.

  4. The agent can act. A successful tools/list request shows that the session is established. A successful tools/call, such as taking a screenshot, shows that the agent can act on the desktop.

Troubleshooting

Troubleshooting agent identity on domain-joined fleets
Symptom Possible cause Solution
The SAML exchange fails, but the OIDC exchange works The agent application doesn't have consent to the access_as_agent scope of the SAML application, or the SAML application has no application-specific signing certificate. Grant the consent and add the certificate, as described in Step 1: Create a SAML application in your identity provider.
The MCP request fails with 403 Access denied by IAM policy The role doesn't allow agentaccess-mcp actions. Add the permissions in Step 2: Configure IAM.
The streaming session fails with INVALID_AUTH_POLICY The role doesn't allow appstream:Stream on the stack, or the stack ARN in the request isn't the one in the policy. Allow appstream:Stream on the stack, as described in Step 3: Configure IAM for SAML, and send the same stack ARN in _meta.
The session fails with CBA certificate fetch failed WorkSpaces Applications can't issue the logon certificate from your private CA, because certificate-based authentication isn't working for the directory. Confirm that certificate-based authentication works for a human user. For more information, see Certificate-Based Authentication.