{"id":7188,"date":"2026-07-01T13:02:11","date_gmt":"2026-07-01T20:02:11","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/azure-sql\/?p=7188"},"modified":"2026-07-01T13:02:11","modified_gmt":"2026-07-01T20:02:11","slug":"sql-mcp-server-obo-auth","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/azure-sql\/sql-mcp-server-obo-auth\/","title":{"rendered":"Audit Frontier AI Agents with SQL MCP Server"},"content":{"rendered":"<p>All modern AI solutions eventually need agents to query or mutate data in Microsoft SQL. Deciding how to do this safely, for example, <a href=\"\/azure-sql\/sql-mcp-server-nl2sql\">how to handle NL2SQL<\/a>, will be an important choice. Next come the twins: authentication and authorization. In this article, we discuss pass-through agentic authentication with the express goal of ensuring operational logs reflect not the agent, not the MCP server, but the user. In future articles, we will discuss the broad array of authorization controls available in Data API builder (DAB) 2.0 with SQL MCP Server.<\/p>\n<h4>There are <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/data-api-builder\/mcp\/how-to-configure-authentication?tabs=bash\">three approaches to SQL authentication in SQL MCP Server<\/a>.<\/h4>\n<p>The first is a username and password. Although it is slowly waning in popularity, it remains a viable option for certain customers. In this case, the audit logs show the identity provided through the connection string, not the user invoking the operation.<\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">User -(any authentication approach)-&gt; Agent\r\nAgent -(any authentication approach)-&gt; MCP server\r\nMCP server -(passes username and password)-&gt; SQL db\r\nSQL db -(logs username as operator)-&gt; Logs<\/code><\/pre>\n<p>The second is managed identity (MI), which eliminates the password but produces the same logging outcome: the logs identify the application, not the user invoking the data operation.<\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">User -(any authentication approach)-&gt; Agent\r\nAgent -(any authentication approach)-&gt; MCP server\r\nMCP server -(passes MCP server identity)-&gt; SQL db\r\nSQL db -(logs MCP server identity as operator)-&gt; Logs<\/code><\/pre>\n<p>The third approach is On-Behalf-Of (OBO), also known as pass-through authentication, using Microsoft Entra ID. It is more sophisticated to set up, but when enabled, services exchange the incoming user token for downstream tokens, forwarding them from service to service.<\/p>\n<pre class=\"prettyprint language-default\"><code class=\"language-default\">User -(authenticates with Microsoft Entra ID)-&gt; Agent\r\nAgent -(forwards user token)-&gt; MCP server\r\nMCP server -(exchanges token and connects as user)-&gt; SQL db\r\nSQL db -(logs user as operator)-&gt; Logs<\/code><\/pre>\n<p>Ultimately, Azure SQL authenticates the actual calling user, not the application, not the agent, and not the MCP server. When a query or mutation occurs in the database, the resulting audit logs identify the user who invoked the operation.<\/p>\n<h3>Data API builder (DAB) 2.0 with SQL MCP Server OBO support<\/h3>\n<p>Data API builder (DAB) 2.0 with SQL MCP Server supports <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/data-api-builder\/concept\/security\/authenticate-on-behalf-of?tabs=bash\">On-Behalf-Of (OBO) authentication for Microsoft SQL databases<\/a> using Microsoft Entra ID. When enabled, it changes your audit story for agentic apps. Instead of asking \u201cwhich service queried the database?\u201d SQL can answer \u201cwhich user caused this query?\u201d Let\u2019s take a look.<\/p>\n<h4>Agents need accountability<\/h4>\n<p>AI agents can reason, plan, and call tools. But when an agent reaches into production data, the enterprise question is not only \u201cdid the agent have permission?\u201d The better question is \u201cwhose permission did the agent use?\u201d<\/p>\n<p>SQL MCP Server exposes SQL tables, views, and stored procedures through MCP tools. This gives agents a controlled way to interact with data without exposing the database directly. With OBO, that controlled path can also preserve the signed-in user\u2019s identity all the way to Azure SQL.<\/p>\n<h3>Configuring OBO<\/h3>\n<p class=\"isSelectedEnd\">OBO is configured in the DAB data source. The connection string must be a bare Azure SQL connection string. Don\u2019t include <code dir=\"ltr\">User ID<\/code>, <code dir=\"ltr\">Password<\/code>, or <code dir=\"ltr\">Authentication<\/code>. DAB injects the per-user access token when it opens the SQL connection.<\/p>\n<pre dir=\"ltr\"><code dir=\"ltr\">{\r\n  \"data-source\": {\r\n    \"database-type\": \"mssql\",\r\n    \"connection-string\": \"@env('MSSQL_CONNECTION_STRING')\",\r\n    \"user-delegated-auth\": {\r\n      \"enabled\": true,\r\n      \"provider\": \"EntraId\",\r\n      \"database-audience\": \"https:\/\/database.windows.net\"\r\n    }\r\n  }\r\n}<\/code><\/pre>\n<p>Because each user receives a distinct database connection, caching might be an initial concern. However, DAB has a hard configuration validation rule that makes response caching and OBO\/user-delegated authentication mutually exclusive. The validator rejects any configuration that enables both simultaneously. The OBO token cache itself is scoped per user.<\/p>\n<h3>Validating the identity<\/h3>\n<p class=\"isSelectedEnd\">The <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/data-api-builder\/quickstart\/authentication-on-behalf-of\">online documentation OBO sample<\/a> uses a small <code dir=\"ltr\">WhoAmI<\/code> view to prove the point. This view returns the identity SQL sees on the active connection.<\/p>\n<pre dir=\"ltr\"><code dir=\"ltr\">CREATE VIEW [dbo].[WhoAmI] AS\r\nSELECT SUSER_NAME() AS [UserName];<\/code><\/pre>\n<p class=\"isSelectedEnd\">Expose it through DAB for authenticated users.<\/p>\n<pre dir=\"ltr\"><code dir=\"ltr\">{\r\n  \"WhoAmI\": {\r\n    \"source\": {\r\n      \"object\": \"dbo.WhoAmI\",\r\n      \"type\": \"view\",\r\n      \"key-fields\": [ \"UserName\" ]\r\n    },\r\n    \"rest\": {\r\n      \"enabled\": true\r\n    },\r\n    \"permissions\": [\r\n      {\r\n        \"role\": \"authenticated\",\r\n        \"actions\": [ { \"action\": \"read\" } ]\r\n      }\r\n    ]\r\n  }\r\n}<\/code><\/pre>\n<p class=\"isSelectedEnd\">With this, an authenticated request to <code dir=\"ltr\">WhoAmI<\/code> returns the <em>user principal<\/em> name SQL sees. In the app, the user signs in to the browser with Microsoft Entra ID, sends a bearer token to Data API builder (DAB) 2.0 with SQL MCP Server, and DAB exchanges that token for an Azure SQL token for the signed-in user.<\/p>\n<pre class=\"prettyprint language-js\"><code class=\"language-js\">const headers = await getAuthHeaders();\r\n\r\nconst response = await fetch(`${API_URL}\/api\/WhoAmI`, {\r\n    method: \"GET\",\r\n    headers\r\n});\r\n\r\nconst payload = await response.json();\r\nconst sqlUserName = payload.value[0].UserName;\r\n\r\nconsole.log(`SQL sees this request as: ${sqlUserName}`);<\/code><\/pre>\n<p class=\"isSelectedEnd\">The sample\u2019s UI displays the result as \u201cSQL Server sees you as: <a href=\"mailto:user@example.com\">[user@example.com](mailto:user@example.com)<\/a>.\u201d The README describes the point plainly: SQL sees the real user, not a service account.<\/p>\n<h3>What this means for MCP<\/h3>\n<p><div class=\"alert alert-info\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>One configuration for all the endpoints<\/strong><\/p>The same DAB runtime can expose REST, GraphQL, and MCP.<\/div><\/p>\n<p class=\"isSelectedEnd\">In the OBO sample, MCP is enabled with the <code dir=\"ltr\">\/mcp<\/code> path in the same configuration that enables Entra ID authentication and user-delegated auth.<\/p>\n<p class=\"isSelectedEnd\">A conceptual MCP tool request can read the same <code dir=\"ltr\">WhoAmI<\/code> entity.<\/p>\n<pre dir=\"ltr\"><code dir=\"ltr\">{\r\n    \"tool\": \"read_records\",\r\n    \"arguments\": {\r\n        \"entity\": \"WhoAmI\"\r\n    }\r\n}<\/code><\/pre>\n<p class=\"isSelectedEnd\">The agent is still using a tool. DAB is still enforcing its entity permissions. Azure SQL still authenticates the user behind the request.<\/p>\n<h4 class=\"isSelectedEnd\">That is the key distinction.<\/h4>\n<p class=\"isSelectedEnd\">The agent performs the action, but SQL records the user context that authorized the action.<\/p>\n<h3>Auditing the result<\/h3>\n<p class=\"isSelectedEnd\">Azure SQL auditing tracks database events and can write them to Blob storage, Event Hubs, or Log Analytics. The audit schema includes fields such as <code dir=\"ltr\">database_principal_name<\/code>, <code dir=\"ltr\">server_principal_name<\/code>, <code dir=\"ltr\">statement<\/code>, and <code dir=\"ltr\">obo_middle_tier_app_id<\/code>, which identifies the middle-tier app that connected using OBO access.<\/p>\n<p class=\"isSelectedEnd\">In <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/azure-sql\/database\/monitoring-sql-database-azure-monitor?view=azuresql\">Log Analytics<\/a>, a simple query can show who SQL saw.<\/p>\n<pre dir=\"ltr\"><code dir=\"ltr\">AzureDiagnostics\r\n  | where Category == \"SQLSecurityAuditEvents\"\r\n  | where database_name_s == \"\"\r\n  | project\r\n  event_time_t,\r\n  action_name_s,\r\n  database_principal_name_s,\r\n  server_principal_name_s,\r\n  obo_middle_tier_app_id_s,\r\n  statement_s\r\n  | order by event_time_t desc<\/code><\/pre>\n<p class=\"isSelectedEnd\">That gives you a better audit frontier for agentic systems. You can see the user, the SQL action, the statement, and the middle-tier app involved in the OBO path.<\/p>\n<h3>Conclusion<\/h3>\n<p class=\"isSelectedEnd\">OBO pass-through authentication makes Data API builder (DAB) 2.0 with SQL MCP Server more than a convenient bridge between agents and data. It makes the bridge accountable. For simple apps, connecting with a managed identity or service credential can be enough. For enterprise agents touching sensitive data, the database often needs to know the real caller. With SQL MCP Server and DAB OBO authentication, Azure SQL can audit the signed-in user behind the agent action. <strong>That means your agent can call tools, DAB can enforce permissions, and SQL can still answer the most important audit question: <em>who did this<\/em>?<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>With On-Behalf-Of authentication, SQL MCP Server lets agents access Microsoft SQL without losing the user identity behind the request. Azure SQL can audit the signed-in user who invoked the operation, not just the agent, app, or MCP server that carried it out.<\/p>\n","protected":false},"author":96788,"featured_media":6602,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[597,720],"tags":[680],"class_list":["post-7188","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-data-api-builder-2","category-sql-mcp-server-2","tag-agentic-ai"],"acf":[],"blog_post_summary":"<p>With On-Behalf-Of authentication, SQL MCP Server lets agents access Microsoft SQL without losing the user identity behind the request. Azure SQL can audit the signed-in user who invoked the operation, not just the agent, app, or MCP server that carried it out.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/7188","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/users\/96788"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/comments?post=7188"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/7188\/revisions"}],"predecessor-version":[{"id":7203,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/posts\/7188\/revisions\/7203"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/media\/6602"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/media?parent=7188"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/categories?post=7188"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sql\/wp-json\/wp\/v2\/tags?post=7188"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}