diff --git a/03-integrations/IDP-examples/EntraID/README.md b/03-integrations/IDP-examples/EntraID/README.md new file mode 100644 index 000000000..0152bbed2 --- /dev/null +++ b/03-integrations/IDP-examples/EntraID/README.md @@ -0,0 +1,89 @@ +# Microsoft Entra ID Integration with Amazon Bedrock AgentCore + +This repository contains three comprehensive notebooks demonstrating how to integrate Microsoft Entra ID (formerly Azure Active Directory) with Amazon Bedrock AgentCore for various authentication and authorization scenarios. + +## What is Microsoft Entra ID? + +Microsoft Entra ID is Microsoft's cloud-based identity and access management service that serves as the central identity provider for Microsoft 365, Azure, and other SaaS applications. + +### Key Features: +- **Single Sign-On (SSO)** - Users authenticate once to access multiple applications +- **Multi-Factor Authentication (MFA)** - Enhanced security through additional verification methods +- **Conditional Access** - Policy-based access control based on user, device, location, and risk +- **Application Integration** - Supports modern authentication protocols like OAuth 2.0, OpenID Connect, and SAML + +### Integration with AgentCore + + +Microsoft Entra ID can be used as an identity provider with AgentCore Identity to: +- Authenticate users before they can invoke agents (inbound authentication) +- Authorize agents to access protected resources on behalf of users (outbound authentication) +- Secure AgentCore Gateway endpoints with JWT-based authorization + +## Example Notebooks Overview + +This learning path includes three practical notebooks that demonstrate different integration patterns: + +### 1. Step By Step MS EntraID and 3LO Outbound for Tools.ipynb + +**Purpose**: Demonstrates how to use Entra ID for **outbound authentication** where AgentCore Runtime deployed agents access external resources (Microsoft OneNote) on behalf of authenticated users. + +**What you'll learn**: +- Setting up Entra ID tenant and application registration +- Creating AgentCore OAuth2 credential providers +- Implementing 3-legged OAuth (3LO) flow for user delegation +- Building agents and deploying on AgentCore Runtime to create and manage OneNote notebooks + +**Key Integration Pattern**: +- User authenticates with Entra ID +- AgentCore Runtime receives delegated permissions to access OneNote API +- AgentCore Runtime agent tools performs actions on user's behalf + + +**Tools Created**: +- `create_notebook` - Creates new OneNote notebooks +- `create_notebook_section` - Adds sections to notebooks +- `add_content_to_notebook_section` - Creates pages with content + +### 2. Step by Step Entra ID for Inbound Auth.ipynb + +**Purpose**: Shows how to use Entra ID for **inbound authentication** to protect AgentCore Runtime agent endpoints, ensuring only authenticated users can invoke agents. + +**What you'll learn**: +- Configuring custom JWT authorizers with Entra ID +- Using MSAL (Microsoft Authentication Library) for device code flow +- Protecting AgentCore Runtime endpoints with bearer tokens +- Managing session-based conversations with authenticated users + +**Key Integration Pattern**: +- Users must authenticate with Entra ID before accessing AgentCore Runtime agents endpoints +- Bearer tokens validate user identity on each request +- Agents remain protected behind authentication layer + + +### 3. Step by Step Entra ID with AgentCore Gateway.ipynb + +**Purpose**: Demonstrates using Entra ID to secure **AgentCore Gateway** endpoints with machine-to-machine (M2M) authentication using client credentials flow. + +**What you'll learn**: +- Setting up Entra ID app roles for API protection +- Configuring AgentCore Gateway with custom JWT authorization +- Creating Lambda functions as MCP (Model Context Protocol) tools +- Using client credentials flow for service-to-service authentication + +**Key Integration Pattern**: +- Applications authenticate using client credentials (no user interaction) +- Gateway validates JWT tokens against Entra ID +- Lambda functions exposed as standardized MCP tools + + + +## Support and Documentation + +- [Microsoft Entra ID Documentation](https://learn.microsoft.com/en-us/entra/) +- [Amazon Bedrock AgentCore Documentation](https://docs.aws.amazon.com/bedrock-agentcore/) +- [OAuth 2.0 Specification](https://oauth.net/2/) + +## Note + +Microsoft Entra ID is not an AWS service. Please refer to Microsoft Entra ID documentation for costs and licensing related to Entra ID usage. \ No newline at end of file diff --git a/03-integrations/IDP-examples/EntraID/Step_By_Step_MS_EntraID_and_3LO_Outbound_for_Tools.ipynb b/03-integrations/IDP-examples/EntraID/Step_By_Step_MS_EntraID_and_3LO_Outbound_for_Tools.ipynb new file mode 100644 index 000000000..706306c3c --- /dev/null +++ b/03-integrations/IDP-examples/EntraID/Step_By_Step_MS_EntraID_and_3LO_Outbound_for_Tools.ipynb @@ -0,0 +1,791 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "df9da464-a67c-469d-9c7b-1fccd31faf89", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## Microsoft Entra ID Overview\n", + "\n", + "Microsoft Entra ID (formerly Azure Active Directory) is Microsoft's cloud-based identity and access management service. It serves as the central \n", + "identity provider for Microsoft 365, Azure, and thousands of other SaaS applications.\n", + "\n", + "Key Features:\n", + "* **Single Sign-On (SSO)** - Users authenticate once to access multiple applications\n", + "* **Multi-Factor Authentication (MFA)** - Enhanced security through additional verification methods\n", + "* **Conditional Access** - Policy-based access control based on user, device, location, and risk\n", + "* **Application Integration** - Supports modern authentication protocols like OAuth 2.0, OpenID Connect, and SAML" + ] + }, + { + "cell_type": "markdown", + "id": "71bd9c9a-024f-4962-87c5-7aa2fedf37e9", + "metadata": {}, + "source": [ + "## Learning Objective\n", + "Microsoft Entra ID can be used as an identity provider on AgentCore Identity and used to authenticate users and have them authorize the agent to acccess protected resources on their behalf. \n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "50bf5b02-a900-41d7-82ce-cbd190b1d0bc", + "metadata": {}, + "source": [ + "## Authorization Code Flow\n", + "The OAuth 2.0 authorization code flow is the recommended approach for web applications to securely authenticate users and obtain access tokens. This \n", + "flow involves:\n", + "1. Redirecting users to Entra ID for authentication\n", + "2. Receiving an authorization code after successful login\n", + "3. Exchanging the code for access and refresh tokens\n", + "4. Using tokens to access protected resources\n", + "\n", + "This integration pattern allows AgentCore to leverage Entra ID's robust identity management capabilities while maintaining secure, standards-based authentication for your applications." + ] + }, + { + "cell_type": "markdown", + "id": "fe317a85-8c09-46c1-ae1b-1181e9b21665", + "metadata": {}, + "source": [ + "## Step 1: Setup Entra ID Tenant" + ] + }, + { + "cell_type": "markdown", + "id": "978ff606-41c9-4770-860a-d2fa00340f02", + "metadata": {}, + "source": [ + "An Entra ID tenant is a dedicated instance of Microsoft Entra ID that represents your organization. Think of it as your organization's isolated directory in Microsoft's cloud.\n", + "\n", + "Key Characteristics:\n", + "* **Unique Identity** - Each tenant has a unique domain (e.g., yourcompany.onmicrosoft.com)\n", + "* **Isolated Boundary** - Users, groups, and applications in one tenant are separate from others\n", + "* **Administrative Control** - Tenant admins manage users, security policies, and application registrations\n", + "* **Multi-Domain Support** - Can include custom domains alongside the default .onmicrosoft.com domain\n", + "\n", + "In Practice:\n", + "When you register an application with Entra ID for OAuth integration, you're registering it within a specific tenant. Users from that tenant can then authenticate against your application using their organizational credentials.\n", + "\n", + "For AgentCore integration, you'll need:\n", + "* **Tenant ID** - Unique identifier for the Entra ID instance\n", + "* **Application Registration** - Your app registered within the tenant\n", + "* **Appropriate Permissions** - Configured access rights for your application\n", + "\n", + "This tenant-based model ensures that authentication and authorization remain within your organization's security boundary.\n", + "\n", + "Steps to create a tenant can be found at https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant\n", + "\n", + "Note:\n", + "1. MS EntraID is not a AWS service. Please refer to Microsoft EntraID documentation for costs related to EntraID.\n", + "2. Screen prints used in the following steps may change. We encourage you to refer to Microsoft Entra ID documentation for latest guidance on setting up EntraID application." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e5f245f8-e6f4-4f45-b6d5-998a4af32cb3", + "metadata": {}, + "source": [ + "## Step 2: Setup Application\n", + "1. Go to portal.azure.com and search for \"Entra ID\" in the serch bar at the top of the screen\n", + "\n", + "2. Got to manage --> App Registrations\n", + "\n", + "3. Click \"New Registration\" and fill in the details. Make sure you select the multi tenant option\n", + "- Use \"https://bedrock-agentcore.us-west-2.amazonaws.com/identities/oauth2/callback\" or \"https://bedrock-agentcore.us-east-1.amazonaws.com/identities/oauth2/callback\" as the redirect URL depending on which regiion you will have your agent running.\n", + "\n", + "4. Create a client secret. Copy the client secret and client ID for use in AgentCore.\n", + "\n", + "5. Create SCopes for OAuth. Go to Expose an API --> Add Scope. Copy and save full scope. \n", + "\n", + "6. Add API permissions to allow access to OneNote. \n", + "\n", + "7. Select tokens to issue. \n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "ae322a2a-fb74-49e4-a71b-6bc11e4ee9d1", + "metadata": {}, + "source": [ + "## Step 2 - Create a Bedrock AgentCore Identity Provider\n", + "Update the environmetn variables below using the details from the tenant and application you in Step 1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a5ced5d-7ae6-4cf6-9328-1314914fe902", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "#os.environ[\"client_id\"] = \"73XXXXXX-CCCC-CCCC-CCCC-NNNNNN1645b6\" # Replace with your client ID\n", + "#os.environ[\"secret\"] = \"b4I8Q~CCCCCVVVVVBBBBBNNNNNXXXXXFFFFFEjXa-C\" # Replace with your secret\n", + "# os.environ[\"scope\"] = \"openid profile https://graph.microsoft.com/Notes.ReadWrite.All https://graph.microsoft.com/Notes.Create\" # Replace with your scope\n", + "#os.environ[\"tenant_id\"] = \"bc244f8c-CCCC-VVVV-BBBB-aa7ab5df1f19\"\n", + "#os.environ[\"audience\"] = \"https://graph.microsoft.com\"\n" + ] + }, + { + "cell_type": "markdown", + "id": "b449408a-fe37-411f-85c5-ca62c8fa13f9", + "metadata": {}, + "source": [ + "##### Amazon Bedrock AgentCore Identity provides managed OAuth 2.0 supported providers for both inbound and outbound authentication. Each provider encapsulates the specific authentication protocols, endpoint configurations, and credential formats required for a particular service or identity system. Create an identity provider for use with your agent. A provider abstracts away the complexity of different OAuth 2.0 implementations, API authentication schemes, and token formats, presenting a unified interface to agents while handling the underlying protocol variations and edge cases." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aec927c6-cfc6-4842-a721-438343741f5a", + "metadata": {}, + "outputs": [], + "source": [ + "from bedrock_agentcore.services.identity import IdentityClient\n", + "\n", + "from boto3.session import Session\n", + "import boto3\n", + "boto_session = Session()\n", + "region = boto_session.region_name\n", + "\n", + "#Configure API Key Provider\n", + "identity_client = IdentityClient(region=region)\n", + "\n", + "ms_provider = identity_client.create_oauth2_credential_provider(\n", + " req={\n", + " \"name\": \"ms_entra_oauth_provider\",\n", + " \"credentialProviderVendor\": \"MicrosoftOauth2\",\n", + " \"oauth2ProviderConfigInput\": {\n", + " \"microsoftOauth2ProviderConfig\": {\n", + " \"clientId\": os.environ[\"client_id\"],\n", + " \"clientSecret\": os.environ[\"secret\"]\n", + " }\n", + " }\n", + " }\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "1c104ac6-0d5d-48c8-9c67-69944b6f586d", + "metadata": {}, + "source": [ + "## Step 3: Validate locally\n", + "\n", + "##### AgentCore Identity enables developers to obtain OAuth tokens for either user-delegated access or machine-to-machine authentication based on the configured OAuth 2.0 credential providers. The service will orchestrate the authentication process between the user or application to the downstream authorization server, and it will retrieve and store the resulting token. Once the token is available in the AgentCore Identity vault, authorized agents can retrieve it and use it to authorize calls to resource servers. \n", + "\n", + "##### In the code below, we are are using Entra ID for a user-delegated flow." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "306d919d-d308-4999-aaf4-eb39784e8a21", + "metadata": {}, + "outputs": [], + "source": [ + "from bedrock_agentcore.identity.auth import requires_access_token\n", + "@requires_access_token(\n", + " provider_name=\"ms_entra_oauth_provider\", # replace with your own credential provider name\n", + " auth_flow=\"USER_FEDERATION\",\n", + " scopes = [os.environ[\"scope\"]],\n", + " on_auth_url= lambda x: print(\"\\nPlease copy and paste this URL in your browser:\\n\" + x),\n", + " force_authentication=True,\n", + ")\n", + "def need_access_token(*, access_token: str):\n", + " return access_token" + ] + }, + { + "cell_type": "markdown", + "id": "56ce5391-e07a-4048-9c56-bee42ac64f7d", + "metadata": {}, + "source": [ + "##### `need_access_token(access_token=\"\")` will present a URL that you use to authenticate into Entra ID and get an authorization token for application to use. Once you have authenticated and shared your consent, the authorization code will be available to you. \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "564f180b-aad6-4056-abf6-081d9b5c1006", + "metadata": {}, + "outputs": [], + "source": [ + "id_token = need_access_token(access_token=\"\")" + ] + }, + { + "cell_type": "markdown", + "id": "aa186c55-74f6-4145-9a90-f879ae678162", + "metadata": {}, + "source": [ + "##### You can decode the token and validate it locally. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e84d65bb-0d58-4947-8c85-2528ba3f131e", + "metadata": {}, + "outputs": [], + "source": [ + "import jwt, json\n", + "\n", + "# Decode the token (without verification for inspection purposes)\n", + "# For production, always verify the token's signature and claims\n", + "decoded_token = jwt.decode(id_token, options={\"verify_signature\": False})\n", + "print(\"\\nDecoded Access Token (for inspection):\")\n", + "token = json.dumps(decoded_token, indent=4)\n", + "print(token) " + ] + }, + { + "cell_type": "markdown", + "id": "2f5aced9-694f-4bba-a1c9-aa02eb3fdce4", + "metadata": {}, + "source": [ + "##### Your decoded token from Entra ID should look similar to below.\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "d7209d1c-b528-44fa-bc95-488b0246936a", + "metadata": {}, + "source": [ + "## Step 4 - Put it all together as an Agent on AgentCore Runtime." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06c95817-8db6-4623-98a1-b47110c661aa", + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "from bedrock_agentcore_starter_toolkit import Runtime\n", + "from boto3.session import Session\n", + "import uuid\n", + "boto_session = Session()\n", + "sts = boto3.client('sts')\n", + "region = boto_session.region_name" + ] + }, + { + "cell_type": "markdown", + "id": "2e090467-0dc4-4693-ac2e-a727cbf4a645", + "metadata": {}, + "source": [ + "### OneNote Integration Agent\n", + "\n", + "This code creates an AI agent that helps users create and manage Microsoft OneNote notebooks through natural language commands. The agent uses EntraID authentication to access the OneNote API and provides three main functions:\n", + "\n", + "1. Create Notebook - Creates a new OneNote notebook (`create_notebook` tool)\n", + "2. Create Section - Adds sections to existing notebooks (`create_notebook_section` tool)\n", + "3. Add Content - Creates pages with content in notebook sections (`add_content_to_notebook_section` tool)\n", + "\n", + "The agent handles OAuth2 authentication automatically, prompting users to authorize when needed, then processes their requests to organize meeting notes\n", + "or other content into structured OneNote notebooks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c639b031-b1a1-49ab-b78d-3be761b4abd2", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile strands_entraid_onenote.py\n", + "import os\n", + "import datetime \n", + "import json\n", + "import asyncio\n", + "import traceback\n", + "import requests\n", + "\n", + "from strands import Agent\n", + "from strands import tool\n", + "from bedrock_agentcore.runtime import BedrockAgentCoreApp\n", + "from bedrock_agentcore.identity.auth import requires_access_token\n", + "from strands.models.bedrock import BedrockModel\n", + "\n", + "os.environ[\"STRANDS_OTEL_ENABLE_CONSOLE_EXPORT\"] = \"true\"\n", + "os.environ[\"OTEL_PYTHON_EXCLUDED_URLS\"] = \"/ping,/invocations\"\n", + "\n", + "# Required OAuth2 scope for Google Calendar API\n", + "SCOPES = ['api://08c1e356-2aad-4702-9ba9-26f9339c0d17/read']\n", + "SCOPES = os.environ[\"scope\"]\n", + "\n", + "entra_access_token = None # Global variable to store the access token\n", + "tool_name = None\n", + "\n", + "\n", + "@tool\n", + "def create_notebook(name: str) -> str:\n", + " \"\"\"\n", + " Create a new Microsoft OneNote notebook for the user. Needed before you can create a section or add content.\n", + " \n", + " Args:\n", + " name (str): The display name for the new notebook\n", + " \n", + " Returns:\n", + " str: The ID of the created notebook\n", + " \"\"\"\n", + " global entra_access_token\n", + " global tool_name \n", + " tool_name = \"create_notebook\"\n", + " # Check if we already have a token\n", + " if not entra_access_token:\n", + " return json.dumps({\"auth_required\": True, \"message\": f\"Entra ID authentication is required for {tool_name}. Please wait while we set up the authorization.\", \"events\": []})\n", + "\n", + " headers = {\n", + " 'Authorization': f'Bearer {entra_access_token}',\n", + " 'Content-Type': 'application/json'\n", + " }\n", + " # Create new notebook\n", + " notebook_data = {'displayName': name}\n", + " notebook = requests.post('https://graph.microsoft.com/v1.0/me/onenote/notebooks', \n", + " headers=headers, json=notebook_data)\n", + " return json.dumps({\"notebook_id\": notebook.json()['id']})\n", + "\n", + "@tool\n", + "def create_notebook_section(notebook_id: str, section_name: str) -> str:\n", + " \"\"\"\n", + " Create a new section in an existing OneNote notebook. Section is created for a specific notebook. \n", + " \n", + " Args:\n", + " notebook_id (str): The ID of the OneNote notebook to create the section in\n", + " section_name (str): The display name for the new section\n", + " \n", + " Returns:\n", + " str: The ID of the created section\n", + " \"\"\"\n", + " global entra_access_token\n", + " global tool_name \n", + " tool_name = \"create_notebook_section\"\n", + " # Check if we already have a token\n", + " if not entra_access_token:\n", + " return json.dumps({\"auth_required\": True, \"message\": f\"Entra ID authentication is required for {tool_name}. Please wait while we set up the authorization.\", \"events\": []})\n", + "\n", + " headers = {\n", + " 'Authorization': f'Bearer {entra_access_token}',\n", + " 'Content-Type': 'application/json'\n", + " }\n", + " # Create new section\n", + " section_data = {'displayName': section_name}\n", + " section = requests.post(f'https://graph.microsoft.com/v1.0/me/onenote/notebooks/{notebook_id}/sections',\n", + " headers=headers, json=section_data)\n", + " section_id = section.json()['id']\n", + " return json.dumps({\"section_id\": section_id})\n", + "\n", + "@tool\n", + "def add_content_to_notebook_section(section_id: str, page_content) -> str:\n", + " \"\"\"\n", + " Add content to a OneNote notebook section by creating a new page.\n", + " \n", + " Args:\n", + " section_id (str): The ID of the OneNote section to add content to\n", + " page_content: The HTML content to add as a new page\n", + " \n", + " Returns:\n", + " str: URL to the created notebook page\n", + " \"\"\"\n", + " global entra_access_token\n", + " global tool_name \n", + " tool_name = \"add_content_to_notebook_section\"\n", + " \n", + " # Check if we already have a token\n", + " if not entra_access_token:\n", + " return json.dumps({\"auth_required\": True, \"message\": f\"Entra ID authentication is required for {tool_name}. Please wait while we set up the authorization.\", \"events\": []})\n", + "\n", + " headers = {\n", + " 'Authorization': f'Bearer {entra_access_token}',\n", + " 'Content-Type': 'text/html'\n", + " }\n", + " page = requests.post(f'https://graph.microsoft.com/v1.0/me/onenote/sections/{section_id}/pages',\n", + " headers=headers, data=page_content)\n", + " url = json.loads(page.text)[\"links\"][\"oneNoteWebUrl\"][\"href\"]\n", + " return json.dumps({\"oneNoteWebUrl\": url})\n", + " \n", + " \n", + "# Initialize the agent with tools\n", + "model = BedrockModel(model_id=\"us.anthropic.claude-3-7-sonnet-20250219-v1:0\")\n", + "system_prompt = \"\"\"You are an Agent who helps user put in their meeting into OneNote notebooks. \n", + " Identify the notebook name, section name and content based on what the user has provided. \n", + " Return notebook URL once created.\"\"\"\n", + "agent = Agent(model=model, system_prompt=system_prompt, tools=[create_notebook, create_notebook_section, add_content_to_notebook_section])\n", + "\n", + "# Initialize app and streaming queue\n", + "app = BedrockAgentCoreApp()\n", + "\n", + "class StreamingQueue:\n", + " def __init__(self):\n", + " self.finished = False\n", + " self.queue = asyncio.Queue()\n", + " \n", + " async def put(self, item):\n", + " await self.queue.put(item)\n", + "\n", + " async def finish(self):\n", + " self.finished = True\n", + " await self.queue.put(None)\n", + "\n", + " async def stream(self):\n", + " while True:\n", + " item = await self.queue.get()\n", + " if item is None and self.finished:\n", + " break\n", + " yield item\n", + "\n", + "queue = StreamingQueue()\n", + "\n", + "async def on_auth_url(url: str):\n", + " print(f\"Authorization url: {url}\")\n", + " await queue.put(f\"Authorization url: {url}\")\n", + "\n", + "\n", + "async def agent_task(user_message: str):\n", + " global tool_name\n", + " try:\n", + " await queue.put(\"Begin agent execution\")\n", + " \n", + " # Call the agent first to see if it needs authentication\n", + " response = agent(user_message)\n", + " \n", + " # Extract text content from the response structure\n", + " response_text = \"\"\n", + " if isinstance(response.message, dict):\n", + " content = response.message.get('content', [])\n", + " if isinstance(content, list):\n", + " for item in content:\n", + " if isinstance(item, dict) and 'text' in item:\n", + " response_text += item['text']\n", + " else:\n", + " response_text = str(response.message)\n", + " \n", + " # Check if the response indicates authentication is required\n", + " # Look for various keywords that indicate authentication issues\n", + " auth_keywords = [\n", + " \"authentication\", \"authorize\", \"authorization\", \"auth\", \n", + " \"sign in\", \"login\", \"access\", \"permission\", \"credential\",\n", + " \"need authentication\", \"requires authentication\"\n", + " ]\n", + " needs_auth = any(keyword.lower() in response_text.lower() for keyword in auth_keywords)\n", + " \n", + " if needs_auth:\n", + " await queue.put(f\"Authentication required for {tool_name} access. Starting authorization flow...\")\n", + " \n", + " # Trigger the 3LO authentication flow\n", + " try:\n", + " global entra_access_token\n", + " entra_access_token = await need_token_3LO_async(access_token=None)\n", + " await queue.put(f\"Authentication successful! Retrying {tool_name}...\")\n", + " \n", + " # Retry the agent call now that we have authentication\n", + " response = agent(user_message)\n", + " except Exception as auth_error:\n", + " # print(\"Exception occurred:\")\n", + " # traceback.print_exc()\n", + " print(f\"auth_error:\", auth_error)\n", + " await queue.put(f\"Authentication failed: {str(auth_error)}\")\n", + " \n", + " await queue.put(response.message)\n", + " await queue.put(\"End agent execution\")\n", + " except Exception as e:\n", + " await queue.put(f\"Error: {str(e)}\")\n", + " finally:\n", + " await queue.finish()\n", + "\n", + "@requires_access_token(\n", + " provider_name=\"ms_entra_oauth_provider\",\n", + " scopes=[os.environ[\"scope\"]],\n", + " auth_flow='USER_FEDERATION',\n", + " on_auth_url=on_auth_url,\n", + " force_authentication=True,\n", + ")\n", + "async def need_token_3LO_async(*, access_token: str):\n", + " global entra_access_token\n", + " entra_access_token = access_token # Update the global access token\n", + " print(\"Got access token....\", access_token)\n", + " return access_token\n", + "\n", + "from fastapi.responses import StreamingResponse\n", + "\n", + "@app.entrypoint\n", + "async def agent_invocation(payload):\n", + " user_message = payload.get(\"prompt\", \"No prompt found in input, please guide customer to create a json payload with prompt key\")\n", + " \n", + " # Create and start the agent task\n", + " task = asyncio.create_task(agent_task(user_message))\n", + " \n", + " \"\"\"# Stream results as they come\n", + " async for item in queue.stream():\n", + " yield f\"data: {json.dumps({'message': str(item)})}\\n\\n\"\n", + " \n", + " # Ensure the task completes\n", + " await task\"\"\"\n", + "\n", + " # Return the stream, but ensure the task runs concurrently\n", + " async def stream_with_task():\n", + " # Stream results as they come\n", + " async for item in queue.stream():\n", + " yield item\n", + " \n", + " # Ensure the task completes\n", + " await task\n", + " \n", + " return stream_with_task()\n", + " \n", + "if __name__ == \"__main__\":\n", + " app.run()" + ] + }, + { + "cell_type": "markdown", + "id": "9c4d9be9-9f12-4d4e-9420-9e94d0e01631", + "metadata": {}, + "source": [ + "##### Configure your AgentCore Runtime" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83f4d49b-7369-447b-b479-753dcffab2c5", + "metadata": {}, + "outputs": [], + "source": [ + "agentcore_runtime = Runtime()\n", + "response = agentcore_runtime.configure(\n", + " entrypoint=\"strands_entraid_onenote.py\",\n", + " #auto_create_execution_role=True,\n", + " execution_role=\"BedrockAgentCoreRole\",\n", + " auto_create_ecr=True,\n", + " requirements_file=\"requirements.txt\",\n", + " region=region,\n", + " agent_name=\"strands_entraid_onenote_3lo\",\n", + " \n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "eefd0b3f-f243-4f75-8068-3d22d60329f3", + "metadata": {}, + "source": [ + "##### Launch your Agent. Once launched, agent would be available for use in your application" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5381e3d8-be49-4f13-828d-74fc49065718", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "launch_response = agentcore_runtime.launch(\n", + " local_build=True, \n", + " auto_update_on_conflict=True,\n", + " env_vars={\n", + " \"scope\": os.environ[\"scope\"],\n", + " }\n", + ")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "e3c3c1cd-3922-4e4c-addd-2e1555e16129", + "metadata": {}, + "source": [ + "#### Below code cell will give you and URL. Copy the URL you get (not the one in the image below), to authenticate using browser window.\n", + "\n", + "\n", + "#### You will be asked to authenticate. Complete the authentication.\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "9e3f58a9-600d-40f6-a0e0-45670c51b0d8", + "metadata": {}, + "source": [ + "#### Make sure you delete notebook with name \"Bedrock Agents\" if it already exists. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee26410c-1412-400e-966b-c32a216e903d", + "metadata": {}, + "outputs": [], + "source": [ + "session_id_1 = str(uuid.uuid1())\n", + "\n", + "prompt = \"\"\"\n", + "Put these notes into onenote notebook named \"Bedrock Agents\".\n", + "\n", + "Amazon Bedrock AgentCore enables you to deploy and operate \n", + "highly capable AI agents securely, at scale. It offers \n", + "infrastructure purpose-built for dynamic agent workloads, \n", + "powerful tools to enhance agents, and essential controls for \n", + "real-world deployment. AgentCore services can be used \n", + " together or independently and work with any framework including \n", + "CrewAI, LangGraph, LlamaIndex, and Strands Agents, as well as \n", + "any foundation model in or outside of Amazon Bedrock, giving you \n", + "ultimate flexibility. AgentCore eliminates the undifferentiated \n", + "heavy lifting of building specialized agent infrastructure, so \n", + "you can accelerate agents to production.\n", + "\"\"\"\n", + "\n", + "st = agentcore_runtime.invoke(\n", + " payload={\"prompt\":prompt}, \n", + " #bearer_token=bearer_token_entra,\n", + " session_id=session_id_1,\n", + " user_id=\"user\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "333db0a8-29c8-46c5-8b8f-07b311c31d80", + "metadata": {}, + "source": [ + "## Step 4 - Validate the created OneNote notebook\n", + "- Agent invoke funtion above will create a Notebook, a Section within, and add some content ot the section.\n", + "- User can access the created notebook by logging into https://sharepoint.com/" + ] + }, + { + "cell_type": "markdown", + "id": "f111c9ff-9cdb-4568-915d-18eb2a190567", + "metadata": {}, + "source": [ + "\n", + "Use the link provided in agent invoke response to access the created Notebook. \n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "d1732725-a624-4495-8206-74087a9577d4", + "metadata": {}, + "source": [ + "## Conclusion and cleanup" + ] + }, + { + "cell_type": "markdown", + "id": "ddf3e0e4-25da-4de3-920d-95bd8145a781", + "metadata": {}, + "source": [ + "In this notebook we learnt how to:\n", + "- Setup Entra ID API and Application to provide OAuth Authorization Code flow\n", + "- Create an AgentCore Runtime and Deployed and agent with tools that acted on user's behalf to create OneNote notebooks" + ] + }, + { + "cell_type": "markdown", + "id": "1474a76e-dc3a-40f7-bd7b-75cc08eb5808", + "metadata": {}, + "source": [ + "#### Resource(s) created" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "344bf9c4-fef5-47dd-a223-674b9a5e4f99", + "metadata": {}, + "outputs": [], + "source": [ + "launch_response.agent_id" + ] + }, + { + "cell_type": "markdown", + "id": "9ddd81eb-694b-410c-b960-ad9c2bb49879", + "metadata": {}, + "source": [ + "#### Delete AgentCore Runtime" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2132974-93f1-4595-a824-7a4cb001b63a", + "metadata": {}, + "outputs": [], + "source": [ + "agentcore_control_client = boto3.client(\"bedrock-agentcore-control\", region_name=region)\n", + "agentcore_control_client.delete_agent_runtime(agentRuntimeId=launch_response.agent_id)" + ] + }, + { + "cell_type": "markdown", + "id": "d1604073-f4bd-4e9e-ab31-ac69c78074f0", + "metadata": {}, + "source": [ + "#### Delete the OAuth2 credential provider" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49951a76-389d-4d18-86c1-b34e7f36c099", + "metadata": {}, + "outputs": [], + "source": [ + "agentcore_control_client.delete_oauth2_credential_provider(name=ms_provider[\"name\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd98530d-bfb3-4980-85f1-1e5b78fa38b7", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7a9d6c5-52ed-41f3-a3e8-439c95959df8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/03-integrations/IDP-examples/EntraID/Step_by_Step_Entra_ID_for_Inbound_Auth.ipynb b/03-integrations/IDP-examples/EntraID/Step_by_Step_Entra_ID_for_Inbound_Auth.ipynb new file mode 100644 index 000000000..be30d3432 --- /dev/null +++ b/03-integrations/IDP-examples/EntraID/Step_by_Step_Entra_ID_for_Inbound_Auth.ipynb @@ -0,0 +1,536 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "05435419-cdcc-4dee-ade7-a2df210cb062", + "metadata": {}, + "source": [ + "## Microsoft EntraID Overview\n", + "\n", + "Microsoft EntraID (formerly Azure Active Directory) is Microsoft's cloud-based identity and access management service. It serves as the central \n", + "identity provider for Microsoft 365, Azure, and thousands of other SaaS applications.\n", + "\n", + "Key Features:\n", + "* **Single Sign-On (SSO)** - Users authenticate once to access multiple applications\n", + "* **Multi-Factor Authentication (MFA)** - Enhanced security through additional verification methods\n", + "* **Conditional Access** - Policy-based access control based on user, device, location, and risk\n", + "* **Application Integration** - Supports modern authentication protocols like OAuth 2.0, OpenID Connect, and SAML" + ] + }, + { + "cell_type": "markdown", + "id": "41194e03-83e6-4365-9611-c5ff5fef7b2e", + "metadata": {}, + "source": [ + "## Learning Objective\n", + "Microsoft Entra ID can be used as an identity provider on AgentCore Identity and used to authenticate users and have them authorize the agent to acccess protected resources on their behalf. In this notebook we will explore the use of Entra ID for inbound authentication - Authenticate users before they can invoke an agent." + ] + }, + { + "cell_type": "markdown", + "id": "47dff909-673f-4f9e-a61c-96bc938750a9", + "metadata": {}, + "source": [ + "## Authorization Code Flow\n", + "The OAuth 2.0 authorization code flow is the recommended approach for web applications to securely authenticate users and obtain access tokens. This \n", + "flow involves:\n", + "1. Redirecting users to EntraID for authentication\n", + "2. Receiving an authorization code after successful login\n", + "3. Exchanging the code for access and refresh tokens\n", + "4. Using tokens to access protected resources\n", + "\n", + "This integration pattern allows AgentCore to leverage EntraID's robust identity management capabilities while maintaining secure, standards-based authentication for your applications." + ] + }, + { + "cell_type": "markdown", + "id": "dff2e98a-1463-446d-be32-3404c0dce371", + "metadata": {}, + "source": [ + "## Learning Objective 1: Setup EntraID for use with AgentCore" + ] + }, + { + "cell_type": "markdown", + "id": "71b555e8-50ac-4f31-bd04-5389a9f31905", + "metadata": {}, + "source": [ + "### Step 1: Setup EntraID Tenant\n", + "An EntraID tenant is a dedicated instance of Microsoft EntraID that represents your organization. Think of it as your organization's isolated directory in Microsoft's cloud.\n", + "\n", + "Key Characteristics:\n", + "* **Unique Identity** - Each tenant has a unique domain (e.g., yourcompany.onmicrosoft.com)\n", + "* **Isolated Boundary** - Users, groups, and applications in one tenant are separate from others\n", + "* **Administrative Control** - Tenant admins manage users, security policies, and application registrations\n", + "* **Multi-Domain Support** - Can include custom domains alongside the default .onmicrosoft.com domain\n", + "\n", + "In Practice:\n", + "When you register an application with EntraID for OAuth integration, you're registering it within a specific tenant. Users from that tenant can then authenticate against your application using their organizational credentials.\n", + "\n", + "For AgentCore integration, you'll need:\n", + "* **Tenant ID** - Unique identifier for the EntraID instance\n", + "* **Application Registration** - Your app registered within the tenant\n", + "* **Appropriate Permissions** - Configured access rights for your application\n", + "\n", + "This tenant-based model ensures that authentication and authorization remain within your organization's security boundary.\n", + "\n", + "Steps to create a tenant can be found at https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant.\n", + "\n", + "Note:\n", + "1. MS EntraID is not a AWS service. Please refer to Microsoft EntraID documentation for costs related to EntraID.\n", + "2. Screen prints used in the following steps may change. We encourage you to refer to Microsoft Entra ID documentation for latest guidance on setting up EntraID application." + ] + }, + { + "cell_type": "markdown", + "id": "7e7a47e9-e543-4f63-aa89-dcc6554f153b", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "### Step 2: Setup Application\n", + "1. Go to portal.azure.com and search for \"EntraID\" in the search bar at the top of the screen\n", + "\n", + "2. Got to manage --> App Registrations\n", + "\n", + "3. Click \"New Registration\" and fill in the details. Make sure you select the multi tenant option\n", + "- Use \"https://bedrock-agentcore.us-west-2.amazonaws.com/identities/oauth2/callback\" or \"https://bedrock-agentcore.us-east-1.amazonaws.com/identities/oauth2/callback\" as the redirect URL depending on which regiion you will have your agent running.\n", + "\n", + "4. Create a client secret. Copy the client secret and client ID for use in AgentCore.\n", + "\n", + "5. Create SCopes for OAuth. Go to Expose an API --> Add Scope. Copy and save full scope. \n", + "\n", + "6. Enable decice code flow.\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "9f7a8b4f-a675-4593-9e5c-f70423f4c729", + "metadata": {}, + "source": [ + "## Learning Objective 2 - Setup a simple agent with EntraID for inbound authentication" + ] + }, + { + "cell_type": "markdown", + "id": "cf31c5b4-4fe7-44c2-b331-1c4080950025", + "metadata": {}, + "source": [ + "#### Prerequisites\n", + "1. Install required packages\n", + "2. Import packages\n", + "3. Get account ID to use throughout the notebook\n", + "4. Set AWS region to \"us-west-2\". You can use any region that supports Bedrock AgentCore. Refer https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-regions.html" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "264182de-4685-4d8c-8043-bea948348f25", + "metadata": {}, + "outputs": [], + "source": [ + "!pip3 install -r requirements.txt --q # in quite mode to reduce output to the console/notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "849b1a17-6153-4263-a3b5-0b536e59fc55", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import uuid\n", + "import boto3\n", + "from boto3.session import Session\n", + "from bedrock_agentcore_starter_toolkit import Runtime\n", + "\n", + "boto_session = Session()\n", + "sts = boto3.client('sts')\n", + "account_id = sts.get_caller_identity().get(\"Account\")\n", + "region = boto_session.region_name" + ] + }, + { + "cell_type": "markdown", + "id": "4ac889f5-9830-4263-9695-40e1e0680705", + "metadata": {}, + "source": [ + "#### Setting environment variables for some key information we will need throughout this notebook. \n", + "Please note that the audience will be same as the \"Application ID URI\" from Step 2.5 above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "518229d7-d516-44a7-be73-82abb9fba4e8", + "metadata": {}, + "outputs": [], + "source": [ + "#os.environ[\"client_id\"] = \"73XXXXXX-CCCC-VVVV-BBBB-NNNNNN1645b6\" # Replace with your client ID\n", + "#os.environ[\"secret\"] = \"bft8Q~XXXXXXXXXXXXXXXXXXXXXXXXXXXXX_ccFb\" # Replace with your secret\n", + "#os.environ[\"scope\"] = \"openid profile https://graph.microsoft.com/Notes.ReadWrite.All https://graph.microsoft.com/Notes.Create\" # Replace with your scope\n", + "#os.environ[\"tenant_id\"] = \"bc244f8c-CCCC-VVVV-BBBB-aa7ab5df1f19\"\n", + "#os.environ[\"audience\"] = \"https://graph.microsoft.com\"" + ] + }, + { + "cell_type": "markdown", + "id": "00a353f9-63f0-4280-beba-ffc015a6c890", + "metadata": {}, + "source": [ + "#### Agent Code\n", + "Keeping the agent simple since the key learning objective for this notebook is to learn inbound authentication using EntraID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8af71d60-1b3a-47cc-bfc8-8874ec639fc9", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile strands_wo_memory.py\n", + "import argparse, json\n", + "from strands import Agent, tool\n", + "from bedrock_agentcore.runtime import BedrockAgentCoreApp\n", + "\n", + "app = BedrockAgentCoreApp()\n", + "\n", + "agent = Agent()\n", + "\n", + "@app.entrypoint\n", + "def strands_agent_bedrock(payload, context):\n", + " print(\"Context object is ....\", context)\n", + " prompt = payload.get(\"prompt\",\"hello\")\n", + " response = agent(prompt)\n", + " return response\n", + "\n", + "if __name__ == \"__main__\":\n", + " app.run()" + ] + }, + { + "cell_type": "markdown", + "id": "9aec9600-d682-4842-b5ff-bc792979b45d", + "metadata": {}, + "source": [ + "#### Configure your runtime with `authorizer_configuration` to enforce inbound authentication. \n", + "You will use a `customJWTAuthorizer` for inbound authentication using EntraID. Note how the discovery_url is building using Tenant ID" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a009b538-fdd6-4c5c-bb94-f141c4ef7fd3", + "metadata": {}, + "outputs": [], + "source": [ + "agentcore_runtime = Runtime()\n", + "discovery_url = f\"https://login.microsoftonline.com/{os.environ[\"tenant_id\"]}/.well-known/openid-configuration\"\n", + "response = agentcore_runtime.configure(\n", + " entrypoint=\"strands_wo_memory.py\",\n", + " #execution_role=\"BedrockAgentCoreRole\",\n", + " auto_create_execution_role=True,\n", + " auto_create_ecr=True,\n", + " requirements_file=\"requirements.txt\",\n", + " region=region,\n", + " agent_name=\"strands_wo_memory_entra_inbound\",\n", + " authorizer_configuration = {\n", + " \"customJWTAuthorizer\": {\n", + " \"discoveryUrl\": discovery_url,\n", + " \"allowedAudience\": [os.environ[\"audience\"]]\n", + " }\n", + " }\n", + ")\n", + "response" + ] + }, + { + "cell_type": "markdown", + "id": "648f60da-52d3-43c4-b58d-7b8b210bac17", + "metadata": {}, + "source": [ + "## Learning Objective 3 - Using MSAL to authenticate and get bearer token\n", + "#### Start authentication flow using device auth code\n", + "\n", + "\n", + "\n", + "#### Login using user ID and password. Or select a user if already logged in.\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "#### Once done, you will get control back into your notebook and bearer token for the user will be available. Similar to the screenshot below. Follow the link and use the code that you get when you run the following cell. \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6cba622d-7517-456a-84da-25dfcdcf877d", + "metadata": {}, + "outputs": [], + "source": [ + "import msal\n", + "import webbrowser\n", + "\n", + "# Configuration details from your Entra ID app registration\n", + "CLIENT_ID = os.environ[\"client_id\"] # Replace with your Application (client) ID\n", + "TENANT_ID = os.environ[\"tenant_id\"] # Replace with your Directory (tenant) ID\n", + "AUTHORITY = f\"https://login.microsoftonline.com/{TENANT_ID}\"\n", + "REDIRECT_URI = f\"https://bedrock-agentcore.{region}.amazonaws.com/identities/oauth2/callback\" # Must match the Redirect URI in your app registration\n", + "SCOPES = [os.environ[\"scope\"]] # Example scopes, adjust as needed\n", + "\n", + "# Create a PublicClientApplication instance\n", + "app = msal.PublicClientApplication(\n", + " client_id=os.environ[\"client_id\"],\n", + " authority=AUTHORITY,\n", + ")\n", + "\n", + "# Attempt to acquire token silently from cache first\n", + "result = app.acquire_token_silent(SCOPES, account=None)\n", + "\n", + "if not result:\n", + " # If no token in cache, initiate interactive flow\n", + " flow = app.initiate_device_flow(scopes=SCOPES)\n", + " #flow = app.initiate_auth_code_flow(scopes=SCOPES)\n", + " if \"user_code\" not in flow:\n", + " raise ValueError(\"Failed to initiate device flow. No user_code found.\")\n", + "\n", + " print(flow[\"message\"])\n", + " webbrowser.open(flow[\"verification_uri\"]) # Open the verification URL in browser\n", + "\n", + " # Wait for user to complete authentication\n", + " result = app.acquire_token_by_device_flow(flow)\n", + "\n", + "if \"access_token\" in result:\n", + " access_token = result[\"access_token\"]\n", + " print(f\"Bearer Token Received: {access_token[:20]}...\")\n", + " # You can now use this access_token to call protected APIs\n", + "else:\n", + " print(result.get(\"error\"))\n", + " print(result.get(\"error_description\"))\n", + " print(result.get(\"correlation_id\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8000478-bf53-45c2-b4e6-a0994bca7fc6", + "metadata": {}, + "outputs": [], + "source": [ + "bearer_token_entra = result[\"access_token\"]" + ] + }, + { + "cell_type": "markdown", + "id": "7ffba18e-4d04-4c60-8998-aa44bd397913", + "metadata": {}, + "source": [ + "## Learning Objective 3 - Deploy agent and invoke using bearer token received earlier" + ] + }, + { + "cell_type": "markdown", + "id": "9474ffe7-4d4d-4e25-9868-7ebb68830bc6", + "metadata": {}, + "source": [ + "#### Deploy Runtime Agent\n", + "Local Docker needs to be running since local_build is enabled. As an alternate, you can have `local_build=False` to use CloudBuild." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee03140c-0b4f-475b-b01e-336aeb45a2d4", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "#strands_wo_memory_launch_response = strands_wo_memory_runtime.launch(local_build=True)\n", + "strands_wo_memory_launch_response = agentcore_runtime.launch(local_build=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f490007-882a-4c23-a691-a3e722f5470d", + "metadata": {}, + "outputs": [], + "source": [ + "import urllib.parse, requests, json\n", + "\n", + "# URL encode the agent ARN\n", + "escaped_agent_arn = urllib.parse.quote(strands_wo_memory_launch_response.agent_arn, safe='')\n", + "\n", + "# Construct the URL\n", + "url = f\"https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{escaped_agent_arn}/invocations?qualifier=DEFAULT\"\n", + "session_id = str(uuid.uuid1())\n", + "headers = {\n", + " \"Authorization\": f\"Bearer {bearer_token_entra}\",\n", + " \"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id\": session_id,\n", + " \"X-Amzn-Trace-Id\": \"1234567890\" \n", + "}\n", + "http_response = requests.post(url, data=json.dumps(\n", + " {\"prompt\":\"Hello! I am John Doe. I like brick oven pizza!\", \"user_id\":\"user1\"}), \n", + " headers=headers\n", + " )\n", + "http_response.text" + ] + }, + { + "cell_type": "markdown", + "id": "1c0c7db5-1dc5-4545-8293-05bbb4dc825a", + "metadata": {}, + "source": [ + "#### Ealier interactions in this session are available through agents.messages. AgentCore memory is not used. Agent will not recollect earlier interaction if a new session ID is used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce024524-e886-401a-a0be-a40c3cac4429", + "metadata": {}, + "outputs": [], + "source": [ + "http_response = requests.post(url, data=json.dumps(\n", + " {\"prompt\":\"Who am I?\", \"user_id\":\"user1\"}), \n", + " headers=headers\n", + " )\n", + "http_response.text" + ] + }, + { + "cell_type": "markdown", + "id": "849f2a19-6623-4fe0-94d6-e0a94fa57486", + "metadata": {}, + "source": [ + "#### As an alternate, you can use the AgentCore Runtime object to invoke the agent. Pass the bearer token and same session sesssion ID to continue with the earlier session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ea58ea9-6922-43e0-9e1c-5131aab292c3", + "metadata": {}, + "outputs": [], + "source": [ + "invoke_response = agentcore_runtime.invoke(\n", + " {\"prompt\":\"Who am I?\", \"user_id\":\"user1\"},\n", + " bearer_token=bearer_token_entra,\n", + " session_id=session_id\n", + ")\n", + "invoke_response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff7d9a9a-7622-40cc-b036-122d7a3cd426", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "3b88ca2d-8dfa-4b5d-8858-cf680c451e37", + "metadata": {}, + "source": [ + "## Conclusion and Cleanup\n", + "In this notebook we learnt how to:\n", + "- Setup Entra ID API and Application to provide OAuth Authorization Code flow\n", + "- Create an AgentCore Runtime and Deployed and agent with inbound authentication using Entra ID\n", + "- Got a token and used it to access the protected Agent" + ] + }, + { + "cell_type": "markdown", + "id": "cf0a23d2-f0af-4474-870c-cd684b44b18b", + "metadata": {}, + "source": [ + "#### Resource(s) created" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f915d5db-d088-4fbc-bdd6-f6ca27e61baf", + "metadata": {}, + "outputs": [], + "source": [ + "strands_wo_memory_launch_response.agent_id" + ] + }, + { + "cell_type": "markdown", + "id": "df610e39-b687-4ecf-855e-0a000d4d2326", + "metadata": {}, + "source": [ + "#### Delete AgentCore Runtime" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b50bf93a-ac46-4c9c-9b45-654060368de7", + "metadata": {}, + "outputs": [], + "source": [ + "agentcore_control_client = boto3.client(\"bedrock-agentcore-control\", region_name=region)\n", + "agentcore_control_client.delete_agent_runtime(agentRuntimeId=strands_wo_memory_launch_response.agent_id)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "273bf288-a36b-4188-b05e-c8a38a8aaf1a", + "metadata": {}, + "outputs": [], + "source": [ + "import jwt, json\n", + "\n", + "# Decode the token (without verification for inspection purposes)\n", + "# For production, always verify the token's signature and claims\n", + "decoded_token = jwt.decode(bearer_token_entra, options={\"verify_signature\": False})\n", + "print(\"\\nDecoded Access Token (for inspection):\")\n", + "token = json.dumps(decoded_token, indent=4)\n", + "print(token) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40fe3acd-b486-41f5-8c4a-ee10878917c3", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/03-integrations/IDP-examples/EntraID/Step_by_Step_Entra_ID_with_AgentCore_Gateway.ipynb b/03-integrations/IDP-examples/EntraID/Step_by_Step_Entra_ID_with_AgentCore_Gateway.ipynb new file mode 100644 index 000000000..4938fdfba --- /dev/null +++ b/03-integrations/IDP-examples/EntraID/Step_by_Step_Entra_ID_with_AgentCore_Gateway.ipynb @@ -0,0 +1,899 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3153aea5-ab75-4756-919f-57543dcfad6c", + "metadata": {}, + "source": [ + "## Microsoft Entra ID Overview\n", + "\n", + "Microsoft Entra ID (formerly Azure Active Directory) is Microsoft's cloud-based identity and access management service. It serves as the central \n", + "identity provider for Microsoft 365, Azure, and thousands of other SaaS applications.\n", + "\n", + "Key Features:\n", + "* **Single Sign-On (SSO)** - Users authenticate once to access multiple applications\n", + "* **Multi-Factor Authentication (MFA)** - Enhanced security through additional verification methods\n", + "* **Conditional Access** - Policy-based access control based on user, device, location, and risk\n", + "* **Application Integration** - Supports modern authentication protocols like OAuth 2.0, OpenID Connect, and SAML" + ] + }, + { + "cell_type": "markdown", + "id": "e25538bd-33d9-47e5-b266-3e19168fdd12", + "metadata": {}, + "source": [ + "## Amazon Bedrock Gateway Overview\n", + "\n", + "Bedrock AgentCore Gateway provides customers a way to turn their existing APIs and Lambda functions into fully-managed MCP servers without needing to manage infra or hosting. Customers can bring OpenAPI spec or Smithy models for their existing APIs, or add Lambda functions that front their tools. Gateway will provide a uniform Model Context Protocol (MCP) interface across all these tools. Gateway employs a dual authentication model to ensure secure access control for both incoming requests and outbound connections to target resources. The framework consists of two key components: Inbound Auth, which validates and authorizes users attempting to access gateway targets, and Outbound Auth, which enables the gateway to securely connect to backend resources on behalf of authenticated users. Together, these authentication mechanisms create a secure bridge between users and their target resources, supporting both IAM credentials and OAuth-based authentication flows. Gateway supports MCP's Streamable HTTP transport connection.\n", + "\n", + "More details on Amazon Bedrock AgentCore Gateway can be found at:\n", + "- https://github.com/awslabs/amazon-bedrock-agentcore-samples/tree/main/01-tutorials/02-AgentCore-gateway\n", + "- https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html" + ] + }, + { + "cell_type": "markdown", + "id": "40b9247b-e3a8-4ae2-938b-d0114183aa61", + "metadata": {}, + "source": [ + "## Learning Objective\n", + "Microsoft EntraID can be used as an identity provider on AgentCore Identity to authorize consuming application's access to protected Amazon AgentCore Gateway resources . In this notebook we will explore the use of EntraID for inbound authentication with Amazon Bedrock Gateway." + ] + }, + { + "cell_type": "markdown", + "id": "9d8c39de-acac-411b-b01f-8bd7baeb4372", + "metadata": {}, + "source": [ + "## Learning Objective 1: Setup Entra ID for use with AgentCore Gateway" + ] + }, + { + "cell_type": "markdown", + "id": "92c40054-305f-4eef-ac3e-b835b679716a", + "metadata": {}, + "source": [ + "### Step 1: Setup Entra ID Tenant\n", + "An Entra ID tenant is a dedicated instance of Microsoft Entra ID that represents your organization. Think of it as your organization's isolated directory in Microsoft's cloud.\n", + "\n", + "Key Characteristics:\n", + "* **Unique Identity** - Each tenant has a unique domain (e.g., yourcompany.onmicrosoft.com)\n", + "* **Isolated Boundary** - Users, groups, and applications in one tenant are separate from others\n", + "* **Administrative Control** - Tenant admins manage users, security policies, and application registrations\n", + "* **Multi-Domain Support** - Can include custom domains alongside the default .onmicrosoft.com domain\n", + "\n", + "In Practice:\n", + "When you register an application with Entra ID for OAuth integration, you're registering it within a specific tenant. Users from that tenant can then authenticate against your application using their organizational credentials.\n", + "\n", + "For AgentCore integration, you'll need:\n", + "* **Tenant ID** - Unique identifier for the Entra ID instance\n", + "* **Application Registration** - Your app registered within the tenant\n", + "* **Appropriate Permissions** - Configured access rights for your application\n", + "\n", + "This tenant-based model ensures that authentication and authorization remain within your organization's security boundary.\n", + "\n", + "Steps to create a tenant can be found at https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant\n", + "\n", + "Note:\n", + "1. MS EntraID is not a AWS service. Please refer to Microsoft EntraID documentation for costs related to EntraID.\n", + "2. Screen prints used in the following steps may change. We encourage you to refer to Microsoft Entra ID documentation for latest guidance on setting up EntraID application." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "308fa87a-fb65-4ad8-91af-536fb4bfbed6", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "os.environ[\"tenant_id\"] = \"bcXXXXXX-CCCC-VVVV-BBBB-NNNNNNdf1f19\" # Replace with Tenant ID from EntraID" + ] + }, + { + "cell_type": "markdown", + "id": "53041059-a82e-417b-9222-fc39b745a904", + "metadata": {}, + "source": [ + "### Step 2: Define the API you want to use\n", + "1. Go to portal.azure.com and search for \"Entra ID\" in the serch bar at the top of the screen\n", + "\n", + "2. Go to manage --> App Registrations\n", + "\n", + "3. Click \"New Registration\" and fill in the details. Select the multi tenant option\n", + "- Do not set any redirect URL. \n", + "\n", + "4. Expose an API through Manage --> Expose an API\n", + "\n", + "5. Create app roles for the API. We are not adding scopes since this is a M2M setup.\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "493510d7-ae62-443e-a6b0-b51317b014c9", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"app_id_uri\"] = \"api://3dXXXXXX-CCCC-VVVV-BBBB-NNNNNN885f25\" # This is the API URL you set up for \"weather_service\"" + ] + }, + { + "cell_type": "markdown", + "id": "d96e8246-f2fc-49cc-8762-c44aad7c487d", + "metadata": {}, + "source": [ + "### Step 3: Create a Entra Client application \n", + "1. Go to portal.azure.com and search for \"Entra ID\" in the search bar at the top of the screen\n", + "\n", + "2. Go to manage --> App Registrations\n", + "\n", + "3. Click \"New Registration\" and fill in the details. Select the multi tenant option\n", + "- Do not set any redirect URL.\n", + "\n", + "4. Create a client secret. Copy the client secret and client ID for use in AgentCore.\n", + "\n", + "5. Go to API permissions and request permissions for the API you created earlier.\n", + "\n", + "\n", + "5. Grant admin consent to use the APIs. \n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "0df318f5-6daa-42d0-badb-bb3f816c4c32", + "metadata": {}, + "source": [ + "5. Set up environment variables using the info from EntraID " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53d93bef-5d5f-4d7d-8e6f-57c5fa5e7440", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "os.environ[\"client_id\"] = \"08XXXXXX-CCCC-VVVV-BBBB-NNNNNNd86cd2\" # Replace with Client ID of the \"weather_service_client\"\n", + "os.environ[\"client_secret\"] = \"muCCCCCVVVVVBBBBBNNNNN3dY6qdlL\" # Replace with Client secret of the \"weather_service_client\"" + ] + }, + { + "cell_type": "markdown", + "id": "f4a1dd59-443b-4495-91aa-9fb48a1bc39a", + "metadata": {}, + "source": [ + "## Learning Objectvie 2: Setup AgentCore Gatway and Lambda Target" + ] + }, + { + "cell_type": "markdown", + "id": "503d3068-5ded-4aae-bc6e-77b6a9191108", + "metadata": {}, + "source": [ + "### Step 1: Create a Lambda Function to sue with Entra ID\n", + "1. Create a python file that we will use as lambda function code. Note how the tool name being called us used retrieved from the `context` object and used in the lambda function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7f3b9b9-3337-4800-86f6-94d4cbbd94b4", + "metadata": {}, + "outputs": [], + "source": [ + "import boto3\n", + "import zipfile\n", + "import io\n", + "from botocore.exceptions import ClientError\n", + "from boto3.session import Session\n", + "import time\n", + "\n", + "boto_session = Session()\n", + "sts = boto3.client('sts')\n", + "region = boto_session.region_name\n", + "account_id = sts.get_caller_identity().get(\"Account\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16880ceb-76fd-472a-ae0f-51ba185c96d6", + "metadata": {}, + "outputs": [], + "source": [ + "%%writefile lambda_function.py\n", + "def lambda_handler(event, context):\n", + " print(f\"Event: {event}\")\n", + " print(f\"Context: {context}\")\n", + " extended_tool_name = context.client_context.custom[\"bedrockAgentCoreToolName\"]\n", + " resource = extended_tool_name.split(\"___\")[1]\n", + "\n", + " print(resource)\n", + " city = event.get(\"city\")\n", + " print(city)\n", + " if resource == \"weather_check\":\n", + " return f\"Weather in {city} is bright and sunny!\"\n", + " elif resource == \"directions\":\n", + " return f\"Take I5 south all the way to {city} downtown\"" + ] + }, + { + "cell_type": "markdown", + "id": "45df3089-af5f-42f3-a68d-18a83fa64272", + "metadata": {}, + "source": [ + "2. Create the lambda funtion" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff3eb513-3ba3-486c-8842-bbe61efa1edc", + "metadata": {}, + "outputs": [], + "source": [ + "lambda_client = boto3.client('lambda', region_name=region)\n", + "with zipfile.ZipFile('lambda_function.zip', 'w') as zip_file:\n", + " zip_file.write('lambda_function.py', 'lambda_function.py')\n", + "\n", + "with open('lambda_function.zip', 'rb') as zip_file:\n", + " zip_content = zip_file.read()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "011e6384-f399-48f6-815a-11e42e8c38d0", + "metadata": {}, + "outputs": [], + "source": [ + "iam_client = boto3.client('iam', region_name=region)\n", + "\n", + "trust_policy = \"\"\"{\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\n", + " \"Service\": \"lambda.amazonaws.com\"\n", + " },\n", + " \"Action\": \"sts:AssumeRole\"\n", + " }\n", + " ]\n", + "}\n", + "\"\"\"\n", + "\n", + "policy = \"\"\"{\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Action\": [\n", + " \"logs:CreateLogGroup\",\n", + " \"logs:CreateLogStream\",\n", + " \"logs:PutLogEvents\"\n", + " ],\n", + " \"Resource\": \"arn:aws:logs:*:*:*\"\n", + " }\n", + " ]\n", + "}\n", + "\"\"\"\n", + "\n", + "response = iam_client.create_role(\n", + " RoleName='lambda-role',\n", + " AssumeRolePolicyDocument=trust_policy\n", + ")\n", + "\n", + "iam_client.put_role_policy(\n", + " PolicyDocument=policy,\n", + " PolicyName=\"lambda-policy\",\n", + " RoleName=\"lambda-role\"\n", + " )\n", + "\n", + "lambda_role_arn = response['Role']['Arn']\n", + "\n", + "# Wait for role to propagate\n", + "time.sleep(10)\n", + "\n", + "response = lambda_client.create_function(\n", + " FunctionName='m2m-entra-lambda',\n", + " Runtime='python3.12',\n", + " Role=lambda_role_arn,\n", + " Handler='lambda_function.lambda_handler',\n", + " Code={'ZipFile': zip_content},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9724060d-8539-42e3-833f-9288034c2bc4", + "metadata": {}, + "outputs": [], + "source": [ + "lambda_arn = response[\"FunctionArn\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9efeb61d-5f33-430d-9e69-b529e86a3f26", + "metadata": {}, + "outputs": [], + "source": [ + "lambda_arn" + ] + }, + { + "cell_type": "markdown", + "id": "437d13b7-796c-48c2-8a2d-135b40c85310", + "metadata": {}, + "source": [ + "### Step 2: Create Amazon Bedrock AgentCore Gateway with inbound security" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5214741-09ef-4058-a4b7-617d8b8e4afc", + "metadata": {}, + "outputs": [], + "source": [ + "iam_client = boto3.client('iam')\n", + "\n", + "trust_policy = \"\"\"{\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Effect\": \"Allow\",\n", + " \"Principal\": {\n", + " \"Service\": \"bedrock-agentcore.amazonaws.com\"\n", + " },\n", + " \"Action\": \"sts:AssumeRole\"\n", + " }\n", + " ]\n", + "}\n", + "\"\"\"\n", + "\n", + "# Create role with trust policy\n", + "response = iam_client.create_role(\n", + " RoleName='bedrock-agent-lambda-role',\n", + " AssumeRolePolicyDocument=trust_policy\n", + ")\n", + "\n", + "permission = \"\"\"{\n", + " \"Version\": \"2012-10-17\",\n", + " \"Statement\": [\n", + " {\n", + " \"Action\": [\n", + " \"lambda:InvokeFunction\"\n", + " ],\n", + " \"Resource\": [\n", + " \"%s\"\n", + " ],\n", + " \"Effect\": \"Allow\",\n", + " \"Sid\": \"InvokeFunction\"\n", + " }\n", + " ]\n", + "}\n", + "\"\"\"% lambda_arn\n", + "\n", + "\n", + "# Add Lambda invoke policy\n", + "iam_client.put_role_policy(\n", + " RoleName='bedrock-agent-lambda-role',\n", + " PolicyName='lambda-invoke-policy',\n", + " PolicyDocument=permission\n", + ")\n", + "\n", + "role_arn = response['Role']['Arn']\n", + "print(f\"Role ARN: {role_arn}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d269f127-c547-489b-aad8-5ee4220e4b8b", + "metadata": {}, + "outputs": [], + "source": [ + "gateway_client = boto3.client(\n", + " \"bedrock-agentcore-control\",\n", + " region_name=region,\n", + ")\n", + "\n", + "gateway_name = \"m2m-entra-gateway\"\n", + "auth_config = {\n", + " \"customJWTAuthorizer\": {\n", + " \"allowedAudience\": [\n", + " os.environ[\"app_id_uri\"]\n", + " ],\n", + " \"discoveryUrl\": f\"https://login.microsoftonline.com/{os.environ[\"tenant_id\"]}/.well-known/openid-configuration\"\n", + " }\n", + "}\n", + "\n", + "create_response = gateway_client.create_gateway(\n", + " name=gateway_name,\n", + " roleArn= role_arn,\n", + " protocolType=\"MCP\",\n", + " authorizerType=\"CUSTOM_JWT\",\n", + " authorizerConfiguration=auth_config,\n", + " description=\"Customer Support AgentCore Gateway\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f665e151-1e4e-4592-b560-5476cf71db10", + "metadata": {}, + "outputs": [], + "source": [ + "gateway_url = create_response[\"gatewayUrl\"]\n", + "gateway_id = create_response[\"gatewayId\"]" + ] + }, + { + "cell_type": "markdown", + "id": "525bd32e-9498-47db-9f2a-22ebb40fba57", + "metadata": {}, + "source": [ + "### Step 3: Add lambda target to the AgentCore Gateway we just created" + ] + }, + { + "cell_type": "markdown", + "id": "1cebd3c4-939c-45bf-aa7d-713b29ac5777", + "metadata": {}, + "source": [ + "1. API Specification for the actual tools we are creating through the lambda function. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef37bd2a-8bbc-4d8f-880e-c88a59fa7bb9", + "metadata": {}, + "outputs": [], + "source": [ + "api_spec = [\n", + " {\n", + " \"name\": \"weather_check\",\n", + " \"description\": \"Check the weather for a given City\",\n", + " \"inputSchema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"city\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The city you want to get weather for\"\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"city\"\n", + " ]\n", + " }\n", + " },\n", + " {\n", + " \"name\": \"directions\",\n", + " \"description\": \"Search the web for directions to a city\",\n", + " \"inputSchema\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"city\": {\n", + " \"type\": \"string\",\n", + " \"description\": \"The city you want to get directions to\"\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"city\"\n", + " ]\n", + " }\n", + " }\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5639fc1-f0e3-4199-b21c-9960d6e75bf7", + "metadata": {}, + "outputs": [], + "source": [ + "lambda_target_config = {\n", + " \"mcp\": {\n", + " \"lambda\": {\n", + " \"lambdaArn\": lambda_arn,\n", + " \"toolSchema\": {\"inlinePayload\": api_spec},\n", + " }\n", + " }\n", + "}\n", + "\n", + "# Create gateway target\n", + "credential_config = [{\"credentialProviderType\": \"GATEWAY_IAM_ROLE\"}]\n", + "\n", + "create_target_response = gateway_client.create_gateway_target(\n", + " gatewayIdentifier=gateway_id,\n", + " name=\"LambdaUsingSDK\",\n", + " description=\"Lambda Target using SDK\",\n", + " targetConfiguration=lambda_target_config,\n", + " credentialProviderConfigurations=credential_config,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "22307a5b-222a-406c-b2a5-cbea91308440", + "metadata": {}, + "source": [ + "## Learning Objective 3: Use the tools made available through AgentCore Gateway in your agent" + ] + }, + { + "cell_type": "markdown", + "id": "42d28cb8-2d09-4941-96d5-58dcf815ea19", + "metadata": {}, + "source": [ + "### Step 1: Get a token and review the payload and header\n", + "1. Get an access token and use it to access AgentCore Gateway. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3339b82c-073b-4b52-8a08-2760bdeac1bd", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import json\n", + "\n", + "TOKEN_URL = f\"https://login.microsoftonline.com/{os.environ[\"tenant_id\"]}/oauth2/v2.0/token\"\n", + "SCOPE=f\"{os.environ[\"app_id_uri\"]}/.default\"\n", + "\n", + "def fetch_access_token(client_id, client_secret, token_url,scope):\n", + "\n", + " data = {\n", + " \"grant_type\":\"client_credentials\",\n", + " \"client_id\":client_id,\n", + " \"client_secret\": client_secret,\n", + " \"scope\":scope\n", + " }\n", + " \n", + " response = requests.post(\n", + " token_url,\n", + " data=data,\n", + " headers={'Content-Type': 'application/x-www-form-urlencoded'}\n", + " )\n", + " #print(response.text)\n", + " return response.json()['access_token']\n", + "\n", + "access_token = fetch_access_token(os.environ[\"client_id\"], os.environ[\"client_secret\"], TOKEN_URL, SCOPE)" + ] + }, + { + "cell_type": "markdown", + "id": "82144702-fdd8-41e6-b644-4c5a3b653665", + "metadata": {}, + "source": [ + "2. Decode it and see the contents. Make sure that \"aud\", \"appid\" and \"roles\" match to the one you have setup earlier. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "813e08b2-7599-43c8-add1-fdb4dbd87534", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "import base64\n", + "import json\n", + "\n", + "def decode_jwt_token(token):\n", + " # Split the JWT into parts\n", + " parts = token.split('.')\n", + " \n", + " # Decode header\n", + " header = json.loads(base64.b64decode(parts[0] + '==').decode('utf-8'))\n", + " \n", + " # Decode payload\n", + " payload = json.loads(base64.b64decode(parts[1] + '==').decode('utf-8'))\n", + " \n", + " return header, payload\n", + "\n", + "# Usage\n", + "header, payload = decode_jwt_token(access_token)\n", + "\n", + "print(\"Header:\", json.dumps(header, indent=2))\n", + "print(\"Payload:\", json.dumps(payload, indent=2))\n", + "\n", + "# Check specific claims\n", + "print(f\"Audience: {payload.get('aud')}\")\n", + "print(f\"Issuer: {payload.get('iss')}\")\n", + "print(f\"Expires: {payload.get('exp')}\")\n", + "print(f\"Scopes: {payload.get('scp')}\")\n", + "print(f\"Roles: {payload.get('roles')}\")" + ] + }, + { + "cell_type": "markdown", + "id": "32674f2c-cf26-4cc7-8e5a-629b339f7072", + "metadata": {}, + "source": [ + "### Step 2: Use the access token to get the list of available tools from AgentCore Gateway\n", + "You should see a tool specification similar to the one below. \n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b98e8dd6-5ca4-4c1e-84e6-2951c70fbeda", + "metadata": {}, + "outputs": [], + "source": [ + "def list_tools(gateway_url, access_token):\n", + " headers = {\n", + " \"Content-Type\": \"application/json\",\n", + " \"Authorization\": f\"Bearer {access_token}\"\n", + " }\n", + "\n", + " payload = {\n", + " \"jsonrpc\": \"2.0\",\n", + " \"id\": \"list-tools-request\",\n", + " \"method\": \"tools/list\"\n", + " }\n", + "\n", + " response = requests.post(gateway_url, headers=headers, json=payload)\n", + " return response.json()\n", + "tools = list_tools(gateway_url, access_token)\n", + "print(json.dumps(tools, indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "9c9f5a6f-ea0d-47e8-ba86-f52429a6ede2", + "metadata": {}, + "source": [ + "### Step 3: Create an mcp client, get tool list and use it in a Strands agent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3aacac3-ae9b-4189-97fd-a9992ed43c99", + "metadata": {}, + "outputs": [], + "source": [ + "from mcp.client.streamable_http import streamablehttp_client\n", + "from strands.tools.mcp import MCPClient\n", + "\n", + "# Set up MCP client\n", + "mcp_client = MCPClient(\n", + " lambda: streamablehttp_client(\n", + " gateway_url,\n", + " headers={\"Authorization\": f\"Bearer {access_token}\"},\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "809f0800-7b8f-470a-ac0d-531f18cba097", + "metadata": {}, + "outputs": [], + "source": [ + "mcp_client.start()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ff1b439-109f-455b-b2b9-36b037553923", + "metadata": {}, + "outputs": [], + "source": [ + "mcp_client.list_tools_sync()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "feb68b99-daad-41f7-a320-92307655548d", + "metadata": {}, + "outputs": [], + "source": [ + "from strands import Agent\n", + "agent = Agent(tools=mcp_client.list_tools_sync())" + ] + }, + { + "cell_type": "markdown", + "id": "68d80a7a-6ac5-4194-8470-2f0f364e1013", + "metadata": {}, + "source": [ + "#### Note: Response from Lambda function defined earlier is static. As a result, the response from this agent will be very similar irrespective of the city you name in the prompt." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1e0c6071-5711-47f0-a650-eab2e68fe47c", + "metadata": {}, + "outputs": [], + "source": [ + "agent(\"What is the weather in San Diego?\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a121dba-90a9-49ca-814d-cc2905db41b5", + "metadata": {}, + "outputs": [], + "source": [ + "agent(\"Give me directions to San Diego?\")" + ] + }, + { + "cell_type": "markdown", + "id": "6efbd120-7dd6-4386-993f-3665e3fd9af8", + "metadata": {}, + "source": [ + "## Conclusion and Cleanup\n", + "In this notebook we learnt how to:\n", + "- Setup Entra ID API and Application to provide OAuth Client Credential (M2M) flow\n", + "- Create an AgentCore Gateway\n", + "- Create a lambda function and add it as a target on the AgentCore Gateway we created. Lambda funtions will be available as a MCP tools through AgentCore Gateway.\n", + "- Use MCP client to access tools provided through Gateway, bind the tools to a Strands Agent, and use it to address user queries." + ] + }, + { + "cell_type": "markdown", + "id": "eb74a1b2-a9f5-4836-b4ad-2de22157fe5e", + "metadata": {}, + "source": [ + "#### Resources created" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ee50f01-fd25-4f64-9485-ed80e2b6067d", + "metadata": {}, + "outputs": [], + "source": [ + "lambda_arn, role_arn, gateway_id, lambda_role_arn, create_response[\"gatewayArn\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e5c22e1-ddc8-4c04-8dfa-bca89c52d6f0", + "metadata": {}, + "outputs": [], + "source": [ + "create_target_response[\"targetId\"]" + ] + }, + { + "cell_type": "markdown", + "id": "399f21ac-887b-406f-9818-acbf9bca7607", + "metadata": {}, + "source": [ + "#### Delete lambda target on your Gateway." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1c07715-8e06-4b0f-b00b-5d6f3e4e14c0", + "metadata": {}, + "outputs": [], + "source": [ + "gateway_client.delete_gateway_target(gatewayIdentifier=gateway_id, targetId=create_target_response[\"targetId\"])" + ] + }, + { + "cell_type": "markdown", + "id": "5c7aa440-603e-4f12-aa5e-be2b921c6dd3", + "metadata": {}, + "source": [ + "#### Delete gateway" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c65a5991-9206-4506-8bc3-e1dffa0b7568", + "metadata": {}, + "outputs": [], + "source": [ + "gateway_client.delete_gateway(gatewayIdentifier=gateway_id)" + ] + }, + { + "cell_type": "markdown", + "id": "68eec24e-0974-4cca-982e-03088cf6e122", + "metadata": {}, + "source": [ + "#### Delete lambda funtion you created." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0fa4c3f2-e166-4b1e-92c8-e8cd0c392e55", + "metadata": {}, + "outputs": [], + "source": [ + "function_name = lambda_arn.split(':')[-1]\n", + "lambda_client.delete_function(FunctionName=function_name)" + ] + }, + { + "cell_type": "markdown", + "id": "0d5c06dc-7df2-436d-8617-2872bfcd6ffc", + "metadata": {}, + "source": [ + "#### Delete created roles" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa86ae0d-1513-49cf-b698-2b56ce38d18f", + "metadata": {}, + "outputs": [], + "source": [ + "role_name = lambda_role_arn.split('/')[-1]\n", + "inline = iam_client.list_role_policies(RoleName=role_name)\n", + "for policy_name in inline['PolicyNames']:\n", + " iam_client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)\n", + "iam_client.delete_role(RoleName=role_name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "669ef197-6e0b-4b8e-9025-e912cb267424", + "metadata": {}, + "outputs": [], + "source": [ + "role_name = role_arn.split('/')[-1]\n", + "inline = iam_client.list_role_policies(RoleName=role_name)\n", + "for policy_name in inline['PolicyNames']:\n", + " iam_client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)\n", + "iam_client.delete_role(RoleName=role_name)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/03-integrations/IDP-examples/EntraID/images/Learning 1.jpg b/03-integrations/IDP-examples/EntraID/images/Learning 1.jpg new file mode 100644 index 000000000..9470f6850 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/Learning 1.jpg differ diff --git a/03-integrations/IDP-examples/EntraID/images/Untitled.png b/03-integrations/IDP-examples/EntraID/images/Untitled.png new file mode 100644 index 000000000..6c95535ed Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/Untitled.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/access.token.issue.png b/03-integrations/IDP-examples/EntraID/images/access.token.issue.png new file mode 100644 index 000000000..f84e63348 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/access.token.issue.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/agent_hook_flow.png b/03-integrations/IDP-examples/EntraID/images/agent_hook_flow.png new file mode 100644 index 000000000..660402d15 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/agent_hook_flow.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/agentcore_memory.png b/03-integrations/IDP-examples/EntraID/images/agentcore_memory.png new file mode 100644 index 000000000..1012963be Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/agentcore_memory.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/api.expose.png b/03-integrations/IDP-examples/EntraID/images/api.expose.png new file mode 100644 index 000000000..a1260fe08 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/api.expose.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/api.permissions.png b/03-integrations/IDP-examples/EntraID/images/api.permissions.png new file mode 100644 index 000000000..154e2e988 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/api.permissions.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/api.permissions1.png b/03-integrations/IDP-examples/EntraID/images/api.permissions1.png new file mode 100644 index 000000000..1100a2aaa Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/api.permissions1.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/api.permissions2.png b/03-integrations/IDP-examples/EntraID/images/api.permissions2.png new file mode 100644 index 000000000..12fef4a2b Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/api.permissions2.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/app.registration.form.png b/03-integrations/IDP-examples/EntraID/images/app.registration.form.png new file mode 100644 index 000000000..479fede7c Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/app.registration.form.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/app.registration.png b/03-integrations/IDP-examples/EntraID/images/app.registration.png new file mode 100644 index 000000000..2f7d70e3c Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/app.registration.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/authenticate.and.authorize.png b/03-integrations/IDP-examples/EntraID/images/authenticate.and.authorize.png new file mode 100644 index 000000000..ccc759034 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/authenticate.and.authorize.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/client.register.png b/03-integrations/IDP-examples/EntraID/images/client.register.png new file mode 100644 index 000000000..2c1721613 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/client.register.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/client.secret.png b/03-integrations/IDP-examples/EntraID/images/client.secret.png new file mode 100644 index 000000000..625f8c60b Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/client.secret.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/configure.png b/03-integrations/IDP-examples/EntraID/images/configure.png new file mode 100644 index 000000000..5b8e004a2 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/configure.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/decoded-token.png b/03-integrations/IDP-examples/EntraID/images/decoded-token.png new file mode 100644 index 000000000..f6ff6ce6a Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/decoded-token.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/device.code.flow.png b/03-integrations/IDP-examples/EntraID/images/device.code.flow.png new file mode 100644 index 000000000..9c23b4366 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/device.code.flow.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/entra-notebook-overview.png b/03-integrations/IDP-examples/EntraID/images/entra-notebook-overview.png new file mode 100644 index 000000000..f1cbd8d67 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/entra-notebook-overview.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/entraid.jpg b/03-integrations/IDP-examples/EntraID/images/entraid.jpg new file mode 100644 index 000000000..74dd1cfe1 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/entraid.jpg differ diff --git a/03-integrations/IDP-examples/EntraID/images/expose.api.png b/03-integrations/IDP-examples/EntraID/images/expose.api.png new file mode 100644 index 000000000..f42243768 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/expose.api.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/gather.client.info.png b/03-integrations/IDP-examples/EntraID/images/gather.client.info.png new file mode 100644 index 000000000..afd7d3e55 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/gather.client.info.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/goal.jpg b/03-integrations/IDP-examples/EntraID/images/goal.jpg new file mode 100644 index 000000000..1f95f1aab Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/goal.jpg differ diff --git a/03-integrations/IDP-examples/EntraID/images/invoke.png b/03-integrations/IDP-examples/EntraID/images/invoke.png new file mode 100644 index 000000000..2b182bf25 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/invoke.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/launch.png b/03-integrations/IDP-examples/EntraID/images/launch.png new file mode 100644 index 000000000..42e56df06 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/launch.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/launch_boto3.jpg b/03-integrations/IDP-examples/EntraID/images/launch_boto3.jpg new file mode 100644 index 000000000..809efd7a5 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/launch_boto3.jpg differ diff --git a/03-integrations/IDP-examples/EntraID/images/list.notebooks.png b/03-integrations/IDP-examples/EntraID/images/list.notebooks.png new file mode 100644 index 000000000..4031db376 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/list.notebooks.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/msa_enter_code.png b/03-integrations/IDP-examples/EntraID/images/msa_enter_code.png new file mode 100644 index 000000000..8b0b0971e Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/msa_enter_code.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/msal_bearer_token_received.png b/03-integrations/IDP-examples/EntraID/images/msal_bearer_token_received.png new file mode 100644 index 000000000..91b546277 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/msal_bearer_token_received.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/msal_code.png b/03-integrations/IDP-examples/EntraID/images/msal_code.png new file mode 100644 index 000000000..2d14bc0f9 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/msal_code.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/msal_confirm.png b/03-integrations/IDP-examples/EntraID/images/msal_confirm.png new file mode 100644 index 000000000..173e1d710 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/msal_confirm.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/msal_done.png b/03-integrations/IDP-examples/EntraID/images/msal_done.png new file mode 100644 index 000000000..c20a5fc23 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/msal_done.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/msal_select_user.png b/03-integrations/IDP-examples/EntraID/images/msal_select_user.png new file mode 100644 index 000000000..9f95b515a Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/msal_select_user.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/notebook.content.png b/03-integrations/IDP-examples/EntraID/images/notebook.content.png new file mode 100644 index 000000000..98ea18740 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/notebook.content.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/notebook.section.png b/03-integrations/IDP-examples/EntraID/images/notebook.section.png new file mode 100644 index 000000000..5dec26d6f Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/notebook.section.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/onenote.api.perm.png b/03-integrations/IDP-examples/EntraID/images/onenote.api.perm.png new file mode 100644 index 000000000..ec2a2bae2 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/onenote.api.perm.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/setup.api.png b/03-integrations/IDP-examples/EntraID/images/setup.api.png new file mode 100644 index 000000000..f5bf3eaa5 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/setup.api.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/sharepoint.com.png b/03-integrations/IDP-examples/EntraID/images/sharepoint.com.png new file mode 100644 index 000000000..c81fd64d1 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/sharepoint.com.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/test.txt b/03-integrations/IDP-examples/EntraID/images/test.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/03-integrations/IDP-examples/EntraID/images/test.txt @@ -0,0 +1 @@ + diff --git a/03-integrations/IDP-examples/EntraID/images/tools.spec.png b/03-integrations/IDP-examples/EntraID/images/tools.spec.png new file mode 100644 index 000000000..15182771c Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/tools.spec.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/url.presented.png b/03-integrations/IDP-examples/EntraID/images/url.presented.png new file mode 100644 index 000000000..922f02687 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/url.presented.png differ diff --git a/03-integrations/IDP-examples/EntraID/images/weather.app.role.png b/03-integrations/IDP-examples/EntraID/images/weather.app.role.png new file mode 100644 index 000000000..3d605e6a4 Binary files /dev/null and b/03-integrations/IDP-examples/EntraID/images/weather.app.role.png differ diff --git a/03-integrations/IDP-examples/EntraID/requirements.txt b/03-integrations/IDP-examples/EntraID/requirements.txt new file mode 100644 index 000000000..0731a463d --- /dev/null +++ b/03-integrations/IDP-examples/EntraID/requirements.txt @@ -0,0 +1,9 @@ +strands-agents +strands-agents-tools +uv +boto3 +bedrock-agentcore +bedrock-agentcore-starter-toolkit +tavily-python +uvicorn +fastapi