Add On-Behalf-Of (OBO) user-delegated authentication for SQL Se…#3151
Add On-Behalf-Of (OBO) user-delegated authentication for SQL Se…#3151anushakolan wants to merge 5 commits intomainfrom
Conversation
|
/azp run |
|
Azure Pipelines successfully started running 6 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
Adds On-Behalf-Of (OBO) user-delegated authentication support for MSSQL data sources so DAB can exchange an incoming user JWT for a SQL-scoped token and execute SQL as the end user (enabling RLS/auditing scenarios).
Changes:
- Introduces OBO token acquisition + in-memory caching via
IOboTokenProvider/OboSqlTokenProviderand an MSAL wrapper for testability. - Wires OBO into MSSQL query execution (sets
SqlConnection.AccessToken, disables pooling for OBO-enabled data sources). - Adds config/schema support and validation + unit tests for OBO configuration and token-provider behavior.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Service/Startup.cs | Registers OBO token provider in DI and builds MSAL confidential client. |
| src/Core/Resolvers/MsSqlQueryExecutor.cs | Uses OBO tokens for request-time SQL connections and disables pooling when enabled. |
| src/Core/Resolvers/Factories/QueryManagerFactory.cs | Plumbs optional OBO provider into MSSQL/DWSQL query executors. |
| src/Core/Resolvers/IOboTokenProvider.cs | Defines OBO token provider abstraction used by query execution. |
| src/Core/Resolvers/OboSqlTokenProvider.cs | Implements OBO token acquisition, cache keying, caching, and refresh logic. |
| src/Core/Resolvers/IMsalClientWrapper.cs | Adds MSAL wrapper interface for mocking MSAL in unit tests. |
| src/Core/Resolvers/MsalClientWrapper.cs | Implements wrapper over MSAL IConfidentialClientApplication. |
| src/Core/Configurations/RuntimeConfigValidator.cs | Adds semantic validation rules for user-delegated-auth configuration. |
| src/Config/ObjectModel/DataSource.cs | Extends data source model with user-delegated-auth options and defaults/constants. |
| src/Config/Converters/DataSourceConverterFactory.cs | Deserializes user-delegated-auth block into the data source model. |
| src/Config/DataApiBuilderException.cs | Adds OBO-specific error messages and a new substatus code. |
| schemas/dab.draft.schema.json | Extends JSON schema to allow data-source.user-delegated-auth configuration. |
| src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs | Adds unit tests for OBO-related config validation scenarios. |
| src/Service.Tests/Authentication/OboSqlTokenProviderUnitTests.cs | Adds unit tests for OBO token provider behavior (claims, scopes, caching, refresh). |
| src/Service.Tests/Authentication/Helpers/RuntimeConfigAuthHelper.cs | Adds helper for building runtime configs that include user-delegated auth in tests. |
souvikghosh04
left a comment
There was a problem hiding this comment.
Posting comments so far. Yet to review some remaining part.
|
/azp run |
|
Azure Pipelines successfully started running 6 pipeline(s). |
…ptions and add provider field Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com>
|
/azp run |
|
Azure Pipelines successfully started running 6 pipeline(s). |
souvikghosh04
left a comment
There was a problem hiding this comment.
added additional review comments which needs some changes and discussion. also, the PR description can be shortened. we don't have to mention all tests and functions, instead just categorize or summarize them.
src/Service.Tests/Authentication/OboSqlTokenProviderUnitTests.cs
Outdated
Show resolved
Hide resolved
src/Service.Tests/Authentication/OboSqlTokenProviderUnitTests.cs
Outdated
Show resolved
Hide resolved
src/Service.Tests/Authentication/OboSqlTokenProviderUnitTests.cs
Outdated
Show resolved
Hide resolved
Aniruddh25
left a comment
There was a problem hiding this comment.
Leaving comments so far
|
/azp run |
|
Azure Pipelines successfully started running 6 pipeline(s). |
Aniruddh25
left a comment
There was a problem hiding this comment.
Consider if we can use Fusion InMemory Cache.
| _dataSourceAccessTokenUsage[dataSourceName] = ShouldManagedIdentityAccessBeAttempted(builder); | ||
|
|
||
| // Track user-delegated authentication settings | ||
| if (dataSource.IsUserDelegatedAuthEnabled && dataSource.UserDelegatedAuth is not null) |
There was a problem hiding this comment.
why is the second check needed? dataSource.UserDelegatedAuth is not null - this is already covered in the first condition, no?
| if (isInRequestContext) | ||
| { | ||
| // Validate database-audience is configured before attempting OBO | ||
| if (string.IsNullOrWhiteSpace(userDelegatedAuth.DatabaseAudience)) |
There was a problem hiding this comment.
this validation should be done at startup, no need to check again. this is the core execution logic, need to keep it simple as possible - so that its quick.
| subStatusCode: DataApiBuilderException.SubStatusCodes.OboAuthenticationFailure); | ||
| } | ||
|
|
||
| // At startup/metadata phase (no HTTP context) - fall through to use Managed Identity |
There was a problem hiding this comment.
do this comment is incorrect, we don't necessarily always fall back to Managed identity. We fallback to use the connection string from the config - which could be Managed Identity or whatever DAB is configured with - could be SQL user/password too.
We do recommend managed identity but thats not what this code will fall through to always.
|
|
||
| // Check cache for valid token | ||
| // Refresh if: token older than cache duration OR token expires within early refresh buffer | ||
| if (_tokenCache.TryGetValue(cacheKey, out CachedToken? cached) && !ShouldRefresh(cached)) |
There was a problem hiding this comment.
Although Redis Cache is not needed, we could use the Fusion InMemory Cache - we already use that for storing results of query. It handles the refresh and expiry logic in a better way.
| _tokenCache[cacheKey] = newCachedToken; | ||
|
|
||
| // Periodically clean up expired tokens to prevent unbounded memory growth | ||
| CleanupExpiredTokensIfNeeded(); |
There was a problem hiding this comment.
If we use Fusion Cache, this periodic cleanup is automatically taken care of for us, because when we add the cachedToken in the in-memory cache, we set the expiration time.
| EventHandler handler = null; | ||
| Mock<MsSqlQueryExecutor> queryExecutor | ||
| = new(provider, dbExceptionParser, queryExecutorLogger.Object, httpContextAccessor.Object, handler); | ||
| = new(provider, dbExceptionParser, queryExecutorLogger.Object, httpContextAccessor.Object, handler, null); |
There was a problem hiding this comment.
nit: name the argument which is passed here as null, at all constructors in this file.
| [DataTestMethod] | ||
| [DataRow("", DisplayName = "Empty string audience should fail")] | ||
| [DataRow(" ", DisplayName = "Whitespace audience should fail")] | ||
| public void ValidateUserDelegatedAuth_EmptyDatabaseAudience_ThrowsError(string audience) |
There was a problem hiding this comment.
Arent empty and missing database audience similar tests?
| /// Test to validate that user-delegated-auth with caching enabled throws an error. | ||
| /// </summary> | ||
| [TestMethod] | ||
| public void ValidateUserDelegatedAuth_CachingEnabled_ThrowsError() |
There was a problem hiding this comment.
There was already a test above to ensure Caching is Disabled.
| [DataRow(null, "test-tenant", "test-secret", DisplayName = "Missing DAB_OBO_CLIENT_ID")] | ||
| [DataRow("test-client", null, "test-secret", DisplayName = "Missing DAB_OBO_TENANT_ID")] | ||
| [DataRow("test-client", "test-tenant", null, DisplayName = "Missing DAB_OBO_CLIENT_SECRET")] | ||
| [DataRow("", "test-tenant", "test-secret", DisplayName = "Empty DAB_OBO_CLIENT_ID")] |
There was a problem hiding this comment.
Please inform @JerryNixon about the need for these env variables and get an ACK. This affects how the users will be using this feature. So, we need to ensure we are on same page.
| [TestMethod] | ||
| public void ValidateUserDelegatedAuth_AzureEnvironment_PassesValidation() | ||
| { | ||
| // Arrange - Simulate Azure environment (IDENTITY_ENDPOINT is set in Azure Container Apps/App Service) |
There was a problem hiding this comment.
How is this test different than above apart from setting the IDENTITY_ENDPOINT? It is testing the same codepath, how is setting IDENTITY_ENDPOINT affecting validation?
Why make this change?
Closes #3122
Data API Builder currently supports managed identity authentication to SQL Server/Azure SQL, where the DAB service itself authenticates using its own identity. However, this approach doesn't support user-level authorization scenarios where:
Row-Level Security (RLS) policies need to filter data based on the actual end user's identity
Database audit logs should reflect the actual user who performed the operation
Fine-grained permissions need to be enforced per-user at the database level
This PR introduces On-Behalf-Of (OBO) user-delegated authentication for SQL Server, enabling DAB to exchange the incoming user's access token for a downstream SQL token using the OAuth 2.0 OBO flow. This allows the actual user's identity to flow through to the database, enabling true user-level authorization.
What is this change?
Overview
Implements the OAuth 2.0 On-Behalf-Of (OBO) flow for SQL Server authentication, allowing DAB to:
Configuration Schema
Added new
user-delegated-authconfiguration section under data-source:New Components
Key Implementation Details
ConcurrentDictionarycache is used to avoid repeated OBO token exchanges for the same user. Cache keys are derived from user identity claims (oid/sub + tenant ID).How was this tested?
Unit Tests (14 tests)
Added comprehensive unit tests in OboSqlTokenProviderUnitTests.cs:
Added validation tests in ConfigValidationUnitTests.cs:
Manual End-to-End Testing (Azure)
Test Environment Setup
Deployed DAB to Azure Container Apps with OBO authentication enabled, connected to an Azure SQL Database configured with Row-Level Security (RLS). The RLS policy filters data based on the authenticated user's email address.
Test Scenarios Validated
User Identity Propagation: Verified that the actual user's identity flows through to the database. When User A calls the API, they only see their own data (1 row out of 6 total rows in the table).
Multi-User Isolation: Tested with multiple users - each user only sees rows where their email matches the UserEmail column, confirming RLS works correctly with OBO tokens.
Authentication Enforcement:
Authorization Enforcement: Users with valid tokens but no SQL database permissions receive a 500 error with login failed message (SQL Error 18456).
Token Caching: Confirmed MSAL token caching is working - initial requests take ~500ms for token exchange, while subsequent requests for the same user complete in 75-150ms.
Database Audit Trail: Verified that SQL Server audit logs show the actual user's identity (e.g., user@example.com rather than the managed identity, enabling proper audit compliance.
Comparison with Managed Identity
Deployed a separate instance using managed identity (without OBO) to compare behavior:
This validates that OBO correctly enables per-user data isolation at the database level.