diff --git a/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addClaimClientId.png b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addClaimClientId.png new file mode 100644 index 000000000..4256bb991 Binary files /dev/null and b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addClaimClientId.png differ diff --git a/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addClaimScope.png b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addClaimScope.png new file mode 100644 index 000000000..7038173ee Binary files /dev/null and b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addClaimScope.png differ diff --git a/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addPerson.png b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addPerson.png new file mode 100644 index 000000000..7b1b5cc0b Binary files /dev/null and b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addPerson.png differ diff --git a/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addPersonForm.png b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addPersonForm.png new file mode 100644 index 000000000..8d6e08628 Binary files /dev/null and b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addPersonForm.png differ diff --git a/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addScope.png b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addScope.png new file mode 100644 index 000000000..e76c8895f Binary files /dev/null and b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/addScope.png differ diff --git a/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/appIntegrationForm1.png b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/appIntegrationForm1.png new file mode 100644 index 000000000..9aa0e20de Binary files /dev/null and b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/appIntegrationForm1.png differ diff --git a/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/appIntegrationForm2.png b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/appIntegrationForm2.png new file mode 100644 index 000000000..784bc7e60 Binary files /dev/null and b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/appIntegrationForm2.png differ diff --git a/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/appIntegrationForm3.png b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/appIntegrationForm3.png new file mode 100644 index 000000000..3d294f692 Binary files /dev/null and b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/appIntegrationForm3.png differ diff --git a/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/createAppIntegration.png b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/createAppIntegration.png new file mode 100644 index 000000000..fc5ae5647 Binary files /dev/null and b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/assets/images/createAppIntegration.png differ diff --git a/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/okta-auth-code-flow-gateway.ipynb b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/okta-auth-code-flow-gateway.ipynb new file mode 100644 index 000000000..8d7bcd3aa --- /dev/null +++ b/01-tutorials/02-AgentCore-gateway/17-inbound-auth-code-flow-okta/okta-auth-code-flow-gateway.ipynb @@ -0,0 +1,469 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "intro-cell", + "metadata": {}, + "source": [ + "# Building Secure Auth Code Flow with AgentCore Gateway & Okta for MCP Clients\n", + "\n", + "This notebook walks through the implementation of the blog post **\"Building Secure Auth Code Flow setup using AgentCore Gateway with MCP clients\"**, using **Okta** as the Identity Provider.\n", + "\n", + "By the end of this notebook you will have:\n", + "- An Okta OIDC application configured for Authorization Code flow with PKCE\n", + "- An AgentCore Gateway with JWT inbound auth pointing to Okta\n", + "- A working Kiro IDE configuration that authenticates via Okta before accessing Gateway tools\n", + "\n", + "### Prerequisites\n", + "- An AWS account with AgentCore Gateway permissions\n", + "- An Okta developer account (https://developer.okta.com/signup/)\n", + "- `mcp-remote` installed (`npm install -g mcp-remote`)\n", + "- Kiro IDE installed locally\n", + "- `boto3` installed in this Python environment" + ] + }, + { + "cell_type": "markdown", + "id": "step1-header", + "metadata": {}, + "source": [ + "## Step 1: Configure Okta as Your Identity Provider\n", + "\n", + "If you already have an Okta account, log in. Otherwise, browse to https://developer.okta.com/signup/ and select **\"Sign up for Integrator Free Plan\"**.\n", + "\n", + "### 1.1 Create a Test User\n", + "\n", + "1. Log in to your Okta admin console.\n", + "2. Select **Directory** > **People** and click **Add person**.\n", + "\n", + "![Add Person](assets/images/addPerson.png)\n", + "\n", + "3. Fill in the form:\n", + " - For **Activation**, select **Activate now**.\n", + " - Check **I will set password** and set a password.\n", + " - Uncheck **User must change password on first login**.\n", + " - Click **Save**.\n", + "\n", + "![Add Person Form](assets/images/addPersonForm.png)\n", + "\n", + "### 1.2 Create an OIDC Application\n", + "\n", + "1. Select **Applications** > **Create App Integration**.\n", + "\n", + "![Create App Integration](assets/images/createAppIntegration.png)\n", + "\n", + "2. For sign-in method, select **OIDC - OpenID Connect**.\n", + "3. For application type, select **Single-Page Application** and click **Next**.\n", + "\n", + "![App Integration Form - Sign-in Method](assets/images/appIntegrationForm1.png)\n", + "\n", + "> **Important:** You must select **Single-Page Application**, not \"Web Application\". SPA apps are public OAuth clients that use PKCE without requiring a client secret — which is exactly what `mcp-remote` expects. A \"Web Application\" (confidential client) requires a `client_secret` during token exchange, causing the auth flow to fail.\n", + "\n", + "#### Configure the application:\n", + "\n", + " a. **App integration name:** `AgentCore Gateway Client` (or your preferred name).\n", + " \n", + " b. Leave **Proof of possession** unchecked.\n", + " \n", + " c. **Grant type:** Select **Authorization Code** and **Refresh Token**.\n", + " \n", + " d. **Sign-in redirect URI:** Set to:\n", + " ```\n", + " http://localhost:3334/oauth/callback\n", + " ```\n", + " This is the callback URL that `mcp-remote` uses during the OAuth flow with Kiro IDE. Remove any other default redirect URIs.\n", + "\n", + "![App Integration Form - Grant Types & Redirect URI](assets/images/appIntegrationForm2.png)\n", + " \n", + " e. Leave the **Sign-out redirect URI** as is.\n", + " \n", + " f. Under **Assignments**, select **Allow everyone in your organization to access**, leave **Enable immediate access** checked, and click **Save**.\n", + "\n", + "![App Integration Form - Assignments](assets/images/appIntegrationForm3.png)\n", + " \n", + " g. Copy the **Client ID** for later use.\n", + "\n", + "> **Note:** Since this is a Single-Page Application (public client), there is no Client Secret. The flow uses PKCE (Proof Key for Code Exchange) instead, which is the recommended approach for desktop/CLI OAuth clients like `mcp-remote`.\n", + "\n", + "### 1.3 Configure the Authorization Server\n", + "\n", + "1. In the left-hand menu, select **Security** > **API**, and click the name of your authorization server (e.g., `default`).\n", + "\n", + "2. Copy the **Audience** and save it for later use.\n", + " > **Note:** It is recommended to create a new authorization server if you plan to change the audience, so other apps are not affected.\n", + "\n", + "3. Click **Scopes** > **Add Scope**:\n", + " - **Name:** `okta.myAccount.read`\n", + " - Give it a **Display Phrase** and **Description**.\n", + " - Set **User Consent** to **implicit**.\n", + " - Leave other settings at default.\n", + " - Click **Save**.\n", + "\n", + "![Add Scope](assets/images/addScope.png)\n", + "\n", + "4. Click **Claims** and add the following claims:\n", + " - A `client_id` claim\n", + "\n", + "![Add Claim - Client ID](assets/images/addClaimClientId.png)\n", + "\n", + " - A `scope` claim\n", + "\n", + "![Add Claim - Scope](assets/images/addClaimScope.png)\n", + " \n", + " These are needed so the AgentCore Gateway can validate the `cid` custom claim in the JWT token.\n", + "\n", + "5. Click **Access Policies** > **Add New Access Policy**:\n", + " - Enter a **Name** and **Description**, click **Create Policy**.\n", + " - Click **Add rule**, give it a **Rule Name**, and click **Create Rule**.\n", + "\n", + "### 1.4 Save Your Configuration Values\n", + "\n", + "You will need these values for the next steps:\n", + "\n", + "| Value | Where to find it | Example |\n", + "|-------|------------------|---------|\n", + "| **Client ID** | Application > General tab | `0oaz7147z771FZmdQ697` |\n", + "| **Audience** | Security > API > Authorization Server | `api://default` |\n", + "| **Okta Domain** | Top-right of admin console or Settings | `dev-12345678.okta.com` |\n", + "| **Discovery URL** | Derived from domain | `https://{domain}/oauth2/default/.well-known/openid-configuration` |" + ] + }, + { + "cell_type": "markdown", + "id": "step2-header", + "metadata": {}, + "source": [ + "## Step 2: Create AgentCore Gateway with Okta JWT Auth\n", + "\n", + "We'll create a new AgentCore Gateway with JWT-based inbound authentication pointing to your Okta authorization server.\n", + "\n", + "> **Note:** The Gateway's authorizer type cannot be changed after creation, so we configure it with `CUSTOM_JWT` from the start.\n", + "\n", + "Enter your Okta configuration values below and run the cell." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "step2-config", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Discovery URL: https://trial-6312662.okta.com/oauth2/default/.well-known/openid-configuration\n", + "Gateway Name: okta-auth-code-gateway\n" + ] + } + ], + "source": [ + "# --- Enter your Okta values ---\n", + "OKTA_DOMAIN = input(\"Enter your Okta domain (e.g., dev-12345678.okta.com): \")\n", + "OKTA_CLIENT_ID = input(\"Enter your Okta Client ID: \")\n", + "OKTA_AUDIENCE = input(\"Enter your Okta Audience: \")\n", + "\n", + "# --- Gateway values ---\n", + "GATEWAY_NAME = \"okta-auth-code-gateway\"\n", + "GATEWAY_ROLE_ARN = \"arn:aws:iam::265666655061:role/AgentCoreGatewayExecutionRole\"\n", + "REGION = \"us-west-2\"\n", + "\n", + "# Derived values\n", + "DISCOVERY_URL = f\"https://{OKTA_DOMAIN}/oauth2/default/.well-known/openid-configuration\"\n", + "\n", + "print(f\"\\nDiscovery URL: {DISCOVERY_URL}\")\n", + "print(f\"Gateway Name: {GATEWAY_NAME}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "step2-update", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gateway ID: okta-auth-code-gateway-bvgdzbk29p\n", + "Gateway URL: https://okta-auth-code-gateway-bvgdzbk29p.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp\n", + "Status: CREATING\n", + "Auth Type: CUSTOM_JWT\n", + "\n", + "Save the Gateway ID and URL for the next steps.\n" + ] + } + ], + "source": [ + "import boto3\n", + "\n", + "client = boto3.client('bedrock-agentcore-control', region_name=REGION)\n", + "\n", + "response = client.create_gateway(\n", + " name=GATEWAY_NAME,\n", + " roleArn=GATEWAY_ROLE_ARN,\n", + " protocolType=\"MCP\",\n", + " authorizerType=\"CUSTOM_JWT\",\n", + " authorizerConfiguration={\n", + " \"customJWTAuthorizer\": {\n", + " \"discoveryUrl\": DISCOVERY_URL,\n", + " \"allowedAudience\": [OKTA_AUDIENCE],\n", + " \"allowedClients\": [OKTA_CLIENT_ID],\n", + " \"customClaims\": [\n", + " {\n", + " \"inboundTokenClaimName\": \"cid\",\n", + " \"inboundTokenClaimValueType\": \"STRING\",\n", + " \"authorizingClaimMatchValue\": {\n", + " \"claimMatchValue\": {\n", + " \"matchValueString\": OKTA_CLIENT_ID\n", + " },\n", + " \"claimMatchOperator\": \"EQUALS\"\n", + " }\n", + " }\n", + " ]\n", + " }\n", + " }\n", + ")\n", + "\n", + "GATEWAY_ID = response['gatewayId']\n", + "GATEWAY_URL = response['gatewayUrl']\n", + "\n", + "print(f\"Gateway ID: {GATEWAY_ID}\")\n", + "print(f\"Gateway URL: {GATEWAY_URL}\")\n", + "print(f\"Status: {response['status']}\")\n", + "print(f\"Auth Type: {response['authorizerType']}\")\n", + "print(\"\\nSave the Gateway ID and URL for the next steps.\")" + ] + }, + { + "cell_type": "markdown", + "id": "step3-header", + "metadata": {}, + "source": [ + "## Step 3: Verify Gateway Requires Authentication\n", + "\n", + "Send an unauthenticated MCP request to confirm the Gateway requires a valid JWT token.\n", + "\n", + "> **Note:** `GATEWAY_URL` was set in the previous cell from the create response. Make sure you ran Step 2 first." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "step3-verify", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Testing: https://okta-auth-code-gateway-bvgdzbk29p.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp\n", + "\n", + "Status Code: 401\n", + "Headers:\n", + " WWW-Authenticate: Bearer resource_metadata=\"https://okta-auth-code-gateway-bvgdzbk29p.gateway.bedrock-agentcore.us-west-2.amazonaws.com/.well-known/oauth-protected-resource\"\n", + "\n", + "Body: {\"jsonrpc\":\"2.0\",\"id\":0,\"error\":{\"code\":-32001,\"message\":\"Missing Bearer token\"}}\n", + "\n", + "✓ Gateway correctly requires authentication.\n" + ] + } + ], + "source": [ + "import requests\n", + "import json\n", + "\n", + "GATEWAY_URL = f\"https://{GATEWAY_ID}.gateway.bedrock-agentcore.{REGION}.amazonaws.com/mcp\"\n", + "\n", + "print(f\"Testing: {GATEWAY_URL}\\n\")\n", + "\n", + "resp = requests.post(\n", + " GATEWAY_URL,\n", + " headers={\"Content-Type\": \"application/json\"},\n", + " json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"params\": {}, \"id\": 1}\n", + ")\n", + "\n", + "print(f\"Status Code: {resp.status_code}\")\n", + "print(f\"Headers:\")\n", + "for k, v in resp.headers.items():\n", + " if 'auth' in k.lower() or 'www' in k.lower():\n", + " print(f\" {k}: {v}\")\n", + "print(f\"\\nBody: {resp.text[:500]}\")\n", + "\n", + "if resp.status_code == 401:\n", + " print(\"\\n✓ Gateway correctly requires authentication.\")\n", + "else:\n", + " print(f\"\\n⚠ Expected 401, got {resp.status_code}. Check your Gateway configuration.\")" + ] + }, + { + "cell_type": "markdown", + "id": "step4-header", + "metadata": {}, + "source": [ + "## Step 4: Install mcp-remote\n", + "\n", + "`mcp-remote` acts as the OAuth proxy between Kiro IDE and the Gateway. It handles the Authorization Code flow with PKCE, manages token refresh, and forwards authenticated requests.\n", + "\n", + "Run this in your terminal (not in the notebook):\n", + "\n", + "```bash\n", + "npm install -g mcp-remote\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "step5-header", + "metadata": {}, + "source": [ + "## Step 5: Configure Kiro IDE\n", + "\n", + "Add the Gateway to your Kiro MCP configuration. The cell below generates the exact JSON you need.\n", + "\n", + "> **Important for Kiro IDE:** Kiro spawns MCP server processes without inheriting your shell's `PATH` or `HOME`. You must specify the full absolute path to the `mcp-remote` binary and explicitly set `HOME` and `PATH` in the `env` section. Run `which mcp-remote` in your terminal to find the full path." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "step5-config", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Add the following to ~/.kiro/settings/mcp.json:\n", + "\n", + "{\n", + " \"mcpServers\": {\n", + " \"gateway-tools\": {\n", + " \"command\": \"/Users/dssouto/.nvm/versions/node/v23.9.0/bin/mcp-remote\",\n", + " \"args\": [\n", + " \"https://okta-auth-code-gateway-bvgdzbk29p.gateway.bedrock-agentcore.us-west-2.amazonaws.com/mcp\",\n", + " \"3334\",\n", + " \"--static-oauth-client-info\",\n", + " \"{\\\"client_id\\\": \\\"0oa114sk6r11FF3On698\\\", \\\"redirect_uris\\\": [\\\"http://localhost:3334/oauth/callback\\\"], \\\"scope\\\": \\\"openid profile email offline_access\\\"}\"\n", + " ],\n", + " \"env\": {\n", + " \"PATH\": \"/Users/dssouto/.nvm/versions/node/v23.9.0/bin:/usr/local/bin:/usr/bin:/bin\",\n", + " \"HOME\": \"/Users/dssouto\"\n", + " }\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "import json\n", + "import shutil\n", + "import os\n", + "\n", + "MCP_PORT = \"3334\"\n", + "\n", + "# Find the full path to mcp-remote (required for Kiro IDE)\n", + "mcp_remote_path = shutil.which(\"mcp-remote\") or \"mcp-remote\"\n", + "home_dir = os.path.expanduser(\"~\")\n", + "path_env = os.path.dirname(mcp_remote_path) + \":/usr/local/bin:/usr/bin:/bin\"\n", + "\n", + "kiro_config = {\n", + " \"mcpServers\": {\n", + " \"gateway-tools\": {\n", + " \"command\": mcp_remote_path,\n", + " \"args\": [\n", + " GATEWAY_URL,\n", + " MCP_PORT,\n", + " \"--static-oauth-client-info\",\n", + " json.dumps({\n", + " \"client_id\": OKTA_CLIENT_ID,\n", + " \"redirect_uris\": [f\"http://localhost:{MCP_PORT}/oauth/callback\"],\n", + " \"scope\": \"openid profile email offline_access\"\n", + " })\n", + " ],\n", + " \"env\": {\n", + " \"PATH\": path_env,\n", + " \"HOME\": home_dir\n", + " }\n", + " }\n", + " }\n", + "}\n", + "\n", + "config_json = json.dumps(kiro_config, indent=2)\n", + "print(\"Add the following to ~/.kiro/settings/mcp.json:\\n\")\n", + "print(config_json)" + ] + }, + { + "cell_type": "markdown", + "id": "step5-instructions", + "metadata": {}, + "source": [ + "### Apply the configuration\n", + "\n", + "1. Open (or create) `~/.kiro/settings/mcp.json`\n", + "2. Add the `gateway-tools` entry from the output above to the `mcpServers` section\n", + "3. Save the file\n", + "\n", + "> **Tip:** If Kiro IDE is already open, it will automatically detect the config change and try to connect the new MCP server — no restart needed." + ] + }, + { + "cell_type": "markdown", + "id": "step6-header", + "metadata": {}, + "source": [ + "## Step 6: Test the End-to-End Flow\n", + "\n", + "Once you save `mcp.json`, Kiro IDE automatically picks up the new config and starts connecting to the Gateway.\n", + "\n", + "1. Your browser will open to Okta's login page\n", + "2. Authenticate with the test user you created in Step 1\n", + "3. You should see \"Authorization successful! You may close this window\" in the browser\n", + "4. Back in Kiro IDE, check the MCP server status — you should see a green checkmark next to `gateway-tools`\n", + "\n", + "In the MCP Logs, a successful connection looks like:\n", + "```\n", + "[info] [gateway-tools] Successfully connected and synced tools and resources for MCP server\n", + "```\n", + "\n", + "### What's happening behind the scenes:\n", + "\n", + "```\n", + "Kiro IDE → mcp-remote → Gateway returns 401 with auth metadata\n", + " → mcp-remote opens browser to Okta login\n", + " → User authenticates with Okta\n", + " → Okta redirects to http://localhost:3334/oauth/callback with auth code\n", + " → mcp-remote exchanges code for access token (using PKCE)\n", + " → mcp-remote sends tool request with Bearer token\n", + " → Gateway validates JWT (signature, expiry, cid claim)\n", + " → Gateway proxies request to MCP server\n", + " → Response flows back to Kiro IDE\n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/01-tutorials/02-AgentCore-gateway/README.md b/01-tutorials/02-AgentCore-gateway/README.md index 5c39fbf93..48c7cf87e 100644 --- a/01-tutorials/02-AgentCore-gateway/README.md +++ b/01-tutorials/02-AgentCore-gateway/README.md @@ -102,3 +102,4 @@ In these tutorials we will cover the following functionality: - [Transforming AWS Lambda function into MCP tools](01-transform-lambda-into-mcp-tools) - [Transforming APIs into MCP tools](02-transform-apis-into-mcp-tools) - [Discovering MCP tools](03-discover-mcp-tools) +- [Inbound Auth Code Flow with Okta](17-inbound-auth-code-flow-okta) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 17a07870a..d805c6af3 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -106,3 +106,4 @@ - Dumitru Pascu (dumip) - Eitan Sela - Swara Gandhi +- Daniel Suarez Souto (dsuarezsoutoaws)