Skip to content

Commit 2ec5fb6

Browse files
Add On-Behalf-Of (OBO) user-delegated authentication for SQL (#3151)
### Why make this change? Closes #3122 Data API Builder currently supports managed identity authentication to 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: 1. Accept a user's access token from the incoming request 2. Exchange it for a SQL Server-scoped token using MSAL 3. Use that token to authenticate to SQL Server as the actual user **Configuration Schema** Added new `user-delegated-auth` configuration section under data-source: ```json { "data-source": { "database-type": "mssql", "connection-string": "@env('SQL_CONNECTION_STRING')", "user-delegated-auth": { "enabled": true, "provider": "EntraId", "database-audience": "https://database.windows.net" } } } ``` **New Components** - `IOboTokenProvider` - Interface for OBO token exchange - `OboSqlTokenProvider` - Implementation that performs the OBO token exchange using MSAL with FusionCache-based token caching - `IMsalClientWrapper` / `MsalClientWrapper` - Wrapper around MSAL's ConfidentialClientApplication for testability - Schema updates - Added `user-delegated-auth` to `dab.draft.schema.json` **Key Implementation Details** 1. **Token Caching**: Uses FusionCache (L1 in-memory cache) to avoid repeated OBO token exchanges for the same user. Cache keys are derived from user identity claims (oid/sub + tenant ID). Tokens are cached with a 5-minute buffer before expiration. 2. **Scope**: Requests token for `https://database.windows.net/.default` scope 3. **Connection String**: When OBO is configured, the `AccessToken` property is set on `SqlConnection` with the exchanged token 4. **Validation**: - OBO is only supported for `mssql` database type - `database-audience` is required when OBO is enabled - Required environment variables: `DAB_OBO_CLIENTID`, `DAB_OBO_CLIENTSECRET`, `DAB_OBO_TENANTID` ### How was this tested? #### Unit Tests Added comprehensive unit tests in `OboSqlTokenProviderUnitTests.cs`: 1. `GetAccessTokenAsync_ReturnsToken_WhenOboSucceeds` - Happy path - successful token exchange 2. `GetAccessTokenAsync_ThrowsException_WhenOboFails` - Error handling when MSAL fails 3. `GetAccessTokenAsync_UsesCorrectScope` - Verifies SQL scope is requested 4. `GetAccessTokenAsync_PassesUserAssertionCorrectly` - Verifies user token is passed correctly 5. `GetAccessTokenAsync_HandlesNullResult` - Null result handling 6. `GetAccessTokenAsync_HandlesEmptyAccessToken` - Empty token handling Added validation tests in `ConfigValidationUnitTests.cs`: 1. `ValidateUserDelegatedAuthConfig_ValidConfig_NoErrors` - Valid OBO config passes validation 2. `ValidateUserDelegatedAuthConfig_InvalidDatabaseAudience_ReturnsError` - Missing/empty database-audience fails when enabled 3. `ValidateUserDelegatedAuthConfig_NonMsSql_ReturnsError` - OBO only supported for mssql 4. `ValidateUserDelegatedAuthConfig_NullOptions_NoErrors` - No OBO config is valid 5. `ValidateUserDelegatedAuthConfig_DisabledWithMissingFields_NoErrors` - Disabled OBO doesn't require all fields #### 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** 1. **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). 2. **Multi-User Isolation**: Tested with 3 different users - each user only sees rows where their email matches the `UserEmail` column, confirming RLS works correctly with OBO tokens. 3. **Authentication Enforcement**: - Requests without a token receive 403 Forbidden - Requests with invalid/expired tokens receive 401 Unauthorized 4. **Authorization Enforcement**: Users with valid tokens but no SQL database permissions receive a 500 error with login failed message (SQL Error 18456). 5. **Token Caching**: Confirmed FusionCache token caching is working via Information-level logging: - First request logs: `OBO token cache MISS for subject {oid}. Acquiring new token from Azure AD.` - Subsequent requests log: `OBO token cache HIT for subject {oid}.` - Each unique user creates a separate cache entry keyed by their subject ID. 6. **Database Audit Trail**: Verified that SQL Server audit logs show the actual user's identity (e.g., `[email protected]`) 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: | Scenario | Result | |----------|--------| | Managed Identity + RLS | Returns 0 rows (MI identity doesn't match any user) | | Managed Identity + RLS disabled | Returns all 6 rows | | OBO + RLS | Returns only the authenticated user's row(s) | This validates that OBO correctly enables per-user data isolation at the database level. --------- Co-authored-by: Jerry Nixon <[email protected]>
1 parent 06e2251 commit 2ec5fb6

18 files changed

Lines changed: 1531 additions & 13 deletions

schemas/dab.draft.schema.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,28 @@
6161
"maximum": 2147483647
6262
}
6363
}
64+
},
65+
"user-delegated-auth": {
66+
"description": "User-delegated authentication configuration for On-Behalf-Of (OBO) flow. Enables DAB to connect to the database using the calling user's identity.",
67+
"type": ["object", "null"],
68+
"additionalProperties": false,
69+
"properties": {
70+
"enabled": {
71+
"$ref": "#/$defs/boolean-or-string",
72+
"description": "Enable user-delegated authentication (OBO flow).",
73+
"default": false
74+
},
75+
"provider": {
76+
"type": "string",
77+
"description": "Identity provider for user-delegated authentication.",
78+
"enum": ["EntraId"],
79+
"default": "EntraId"
80+
},
81+
"database-audience": {
82+
"type": "string",
83+
"description": "The audience URI for the target database (e.g., https://database.windows.net for Azure SQL)."
84+
}
85+
}
6486
}
6587
},
6688
"allOf": [

src/Cli.Tests/ModuleInitializer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public static void Init()
2323
VerifierSettings.IgnoreMember<DataSource>(dataSource => dataSource.IsDatasourceHealthEnabled);
2424
// Ignore the DatasourceThresholdMs from the output to avoid committing it.
2525
VerifierSettings.IgnoreMember<DataSource>(dataSource => dataSource.DatasourceThresholdMs);
26+
// Ignore the IsUserDelegatedAuthEnabled from the output as it's a computed property.
27+
VerifierSettings.IgnoreMember<DataSource>(dataSource => dataSource.IsUserDelegatedAuthEnabled);
2628
// Ignore the datasource files as that's unimportant from a test standpoint.
2729
VerifierSettings.IgnoreMember<RuntimeConfig>(config => config.DataSourceFiles);
2830
// Ignore the CosmosDataSourceUsed as that's unimportant from a test standpoint.

src/Config/Converters/DataSourceConverterFactory.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,16 @@ public DataSourceConverter(DeserializationVariableReplacementSettings? replaceme
5151
string connectionString = string.Empty;
5252
DatasourceHealthCheckConfig? health = null;
5353
Dictionary<string, object?>? datasourceOptions = null;
54+
UserDelegatedAuthOptions? userDelegatedAuth = null;
5455

5556
while (reader.Read())
5657
{
5758
if (reader.TokenType is JsonTokenType.EndObject)
5859
{
59-
return new DataSource(databaseType, connectionString, datasourceOptions, health);
60+
return new DataSource(databaseType, connectionString, datasourceOptions, health)
61+
{
62+
UserDelegatedAuth = userDelegatedAuth
63+
};
6064
}
6165

6266
if (reader.TokenType is JsonTokenType.PropertyName)
@@ -136,6 +140,20 @@ public DataSourceConverter(DeserializationVariableReplacementSettings? replaceme
136140
datasourceOptions = optionsDict;
137141
}
138142

143+
break;
144+
case "user-delegated-auth":
145+
if (reader.TokenType != JsonTokenType.Null)
146+
{
147+
try
148+
{
149+
userDelegatedAuth = JsonSerializer.Deserialize<UserDelegatedAuthOptions>(ref reader, options);
150+
}
151+
catch (Exception e)
152+
{
153+
throw new JsonException($"Error while deserializing DataSource user-delegated-auth: {e.Message}");
154+
}
155+
}
156+
139157
break;
140158
default:
141159
throw new JsonException($"Unexpected property {propertyName} while deserializing DataSource.");

src/Config/DataApiBuilderException.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public class DataApiBuilderException : Exception
2020
public const string GRAPHQL_MUTATION_FIELD_AUTHZ_FAILURE = "Unauthorized due to one or more fields in this mutation.";
2121
public const string GRAPHQL_GROUPBY_FIELD_AUTHZ_FAILURE = "Access forbidden to field '{0}' referenced in the groupBy argument.";
2222
public const string GRAPHQL_AGGREGATION_FIELD_AUTHZ_FAILURE = "Access forbidden to field '{0}' referenced in the aggregation function '{1}'.";
23+
public const string OBO_IDENTITY_CLAIMS_MISSING = "User-delegated authentication failed: Neither 'oid' nor 'sub' claim found in the access token.";
24+
public const string OBO_TENANT_CLAIM_MISSING = "User-delegated authentication failed: 'tid' (tenant id) claim not found in the access token.";
25+
public const string OBO_TOKEN_ACQUISITION_FAILED = "User-delegated authentication failed: Unable to acquire database access token on behalf of the user.";
26+
public const string OBO_MISSING_USER_CONTEXT = "User-delegated authentication failed: Missing or invalid 'Authorization: Bearer <token>' header. OBO requires a valid user token to exchange for database access.";
27+
public const string OBO_MISSING_DATABASE_AUDIENCE = "User-delegated authentication failed: 'database-audience' is not configured in the data source's user-delegated-auth settings.";
2328

2429
public enum SubStatusCodes
2530
{
@@ -127,7 +132,11 @@ public enum SubStatusCodes
127132
/// <summary>
128133
/// Error due to client input validation failure.
129134
/// </summary>
130-
DatabaseInputError
135+
DatabaseInputError,
136+
/// <summary>
137+
/// User-delegated (OBO) authentication failed due to missing identity claims.
138+
/// </summary>
139+
OboAuthenticationFailure
131140
}
132141

133142
public HttpStatusCode StatusCode { get; }

src/Config/ObjectModel/DataSource.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ public int DatasourceThresholdMs
4040
}
4141
}
4242

43+
/// <summary>
44+
/// Configuration for user-delegated authentication (OBO) against the
45+
/// configured database.
46+
/// </summary>
47+
[JsonPropertyName("user-delegated-auth")]
48+
public UserDelegatedAuthOptions? UserDelegatedAuth { get; init; }
49+
50+
/// <summary>
51+
/// Indicates whether user-delegated authentication is enabled for this data source.
52+
/// </summary>
53+
[JsonIgnore]
54+
public bool IsUserDelegatedAuthEnabled =>
55+
UserDelegatedAuth is not null && UserDelegatedAuth.Enabled;
56+
4357
/// <summary>
4458
/// Converts the <c>Options</c> dictionary into a typed options object.
4559
/// May return null if the dictionary is null.
@@ -111,3 +125,67 @@ public record CosmosDbNoSQLDataSourceOptions(string? Database, string? Container
111125
/// Options for MsSql database.
112126
/// </summary>
113127
public record MsSqlOptions(bool SetSessionContext = true) : IDataSourceOptions;
128+
129+
/// <summary>
130+
/// Options for user-delegated authentication (OBO) for a data source.
131+
///
132+
/// When OBO is NOT enabled (default): DAB connects to the database using a single application principal,
133+
/// either via Managed Identity or credentials supplied in the connection string. All requests execute
134+
/// under the same database identity regardless of which user made the API call.
135+
///
136+
/// When OBO IS enabled: DAB exchanges the calling user's JWT for a database access token using the
137+
/// On-Behalf-Of flow. This allows DAB to connect to the database as the actual user, enabling
138+
/// Row-Level Security (RLS) filtering based on user identity.
139+
///
140+
/// OBO requires an Azure AD App Registration (separate from the DAB service's Managed Identity).
141+
/// The operator deploying DAB must set the following environment variables for the OBO App Registration,
142+
/// which DAB reads at startup via Environment.GetEnvironmentVariable():
143+
/// - DAB_OBO_CLIENT_ID: The Application (client) ID of the OBO App Registration
144+
/// - DAB_OBO_TENANT_ID: The Directory (tenant) ID where the OBO App Registration is registered
145+
/// - DAB_OBO_CLIENT_SECRET: The client secret of the OBO App Registration (not a user secret)
146+
///
147+
/// These credentials belong to the OBO App Registration, which acts as a confidential client to exchange
148+
/// the incoming user JWT for a database access token. The user provides only their JWT; DAB uses the
149+
/// App Registration credentials to perform the OBO token exchange on their behalf.
150+
///
151+
/// These can be set in the hosting environment (e.g., Azure Container Apps secrets, Kubernetes secrets,
152+
/// Docker environment variables, or local shell environment).
153+
///
154+
/// Note: DAB-specific prefixes (DAB_OBO_*) are used instead of AZURE_* to avoid conflict with
155+
/// DefaultAzureCredential, which interprets AZURE_CLIENT_ID as a User-Assigned Managed Identity ID.
156+
/// At startup (when no user context is available), DAB falls back to Managed Identity for metadata operations.
157+
/// </summary>
158+
/// <param name="Enabled">Whether user-delegated authentication is enabled.</param>
159+
/// <param name="Provider">The authentication provider (currently only EntraId is supported).</param>
160+
/// <param name="DatabaseAudience">Audience used when acquiring database tokens on behalf of the user.</param>
161+
public record UserDelegatedAuthOptions(
162+
[property: JsonPropertyName("enabled")] bool Enabled = false,
163+
[property: JsonPropertyName("provider")] string? Provider = null,
164+
[property: JsonPropertyName("database-audience")] string? DatabaseAudience = null)
165+
{
166+
/// <summary>
167+
/// Default duration, in minutes, to cache tokens for a given delegated identity.
168+
/// With a 5-minute early refresh buffer, tokens are refreshed at the 40-minute mark.
169+
/// </summary>
170+
public const int DEFAULT_TOKEN_CACHE_DURATION_MINUTES = 45;
171+
172+
/// <summary>
173+
/// Environment variable name for OBO App Registration client ID.
174+
/// Uses DAB-specific prefix to avoid conflict with AZURE_CLIENT_ID which is
175+
/// interpreted by DefaultAzureCredential/ManagedIdentityCredential as a
176+
/// User-Assigned Managed Identity ID.
177+
/// </summary>
178+
public const string DAB_OBO_CLIENT_ID_ENV_VAR = "DAB_OBO_CLIENT_ID";
179+
180+
/// <summary>
181+
/// Environment variable name for OBO App Registration client secret.
182+
/// Used for On-Behalf-Of token exchange.
183+
/// </summary>
184+
public const string DAB_OBO_CLIENT_SECRET_ENV_VAR = "DAB_OBO_CLIENT_SECRET";
185+
186+
/// <summary>
187+
/// Environment variable name for OBO tenant ID.
188+
/// Uses DAB-specific prefix for consistency with OBO client ID.
189+
/// </summary>
190+
public const string DAB_OBO_TENANT_ID_ENV_VAR = "DAB_OBO_TENANT_ID";
191+
}

src/Core/Configurations/RuntimeConfigValidator.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ public class RuntimeConfigValidator : IConfigValidator
4949
DatabaseType.DWSQL
5050
];
5151

52+
// Error messages for user-delegated authentication configuration.
53+
public const string USER_DELEGATED_AUTH_DATABASE_TYPE_ERR_MSG =
54+
"User-delegated authentication is only supported when data-source.database-type is 'mssql'.";
55+
56+
public const string USER_DELEGATED_AUTH_MISSING_AUDIENCE_ERR_MSG =
57+
"data-source.user-delegated-auth.database-audience must be set when user-delegated-auth is configured.";
58+
59+
public const string USER_DELEGATED_AUTH_CACHING_ERR_MSG =
60+
"runtime.cache.enabled must be false when user-delegated-auth is configured.";
61+
62+
public const string USER_DELEGATED_AUTH_MISSING_CREDENTIALS_ERR_MSG =
63+
"User-delegated authentication requires DAB_OBO_CLIENT_ID, DAB_OBO_TENANT_ID, and DAB_OBO_CLIENT_SECRET environment variables.";
64+
5265
// Error messages.
5366
public const string INVALID_CLAIMS_IN_POLICY_ERR_MSG = "One or more claim types supplied in the database policy are not supported.";
5467

@@ -119,6 +132,68 @@ public void ValidateDataSourceInConfig(
119132
}
120133

121134
ValidateDatabaseType(runtimeConfig, fileSystem, logger);
135+
136+
ValidateUserDelegatedAuthOptions(runtimeConfig);
137+
}
138+
139+
/// <summary>
140+
/// Validates configuration for user-delegated authentication (OBO).
141+
/// When any data source has user-delegated-auth configured, the following
142+
/// rules are enforced:
143+
/// - data-source.database-type must be "mssql".
144+
/// - data-source.user-delegated-auth.database-audience must be present.
145+
/// - runtime.cache.enabled must be false.
146+
/// - Environment variables DAB_OBO_CLIENT_ID, DAB_OBO_TENANT_ID, and DAB_OBO_CLIENT_SECRET must be set.
147+
/// </summary>
148+
/// <param name="runtimeConfig">Runtime configuration.</param>
149+
private void ValidateUserDelegatedAuthOptions(RuntimeConfig runtimeConfig)
150+
{
151+
foreach (DataSource dataSource in runtimeConfig.ListAllDataSources())
152+
{
153+
// Skip validation if user-delegated-auth is not configured or not enabled
154+
if (dataSource.UserDelegatedAuth is null || !dataSource.UserDelegatedAuth.Enabled)
155+
{
156+
continue;
157+
}
158+
159+
if (dataSource.DatabaseType != DatabaseType.MSSQL)
160+
{
161+
HandleOrRecordException(new DataApiBuilderException(
162+
message: USER_DELEGATED_AUTH_DATABASE_TYPE_ERR_MSG,
163+
statusCode: HttpStatusCode.ServiceUnavailable,
164+
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
165+
}
166+
167+
if (string.IsNullOrWhiteSpace(dataSource.UserDelegatedAuth.DatabaseAudience))
168+
{
169+
HandleOrRecordException(new DataApiBuilderException(
170+
message: USER_DELEGATED_AUTH_MISSING_AUDIENCE_ERR_MSG,
171+
statusCode: HttpStatusCode.ServiceUnavailable,
172+
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
173+
}
174+
175+
// Validate OBO App Registration credentials are configured via environment variables.
176+
string? clientId = Environment.GetEnvironmentVariable(UserDelegatedAuthOptions.DAB_OBO_CLIENT_ID_ENV_VAR);
177+
string? tenantId = Environment.GetEnvironmentVariable(UserDelegatedAuthOptions.DAB_OBO_TENANT_ID_ENV_VAR);
178+
string? clientSecret = Environment.GetEnvironmentVariable(UserDelegatedAuthOptions.DAB_OBO_CLIENT_SECRET_ENV_VAR);
179+
180+
if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(clientSecret))
181+
{
182+
HandleOrRecordException(new DataApiBuilderException(
183+
message: USER_DELEGATED_AUTH_MISSING_CREDENTIALS_ERR_MSG,
184+
statusCode: HttpStatusCode.ServiceUnavailable,
185+
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
186+
}
187+
188+
// Validate caching is disabled when user-delegated-auth is enabled
189+
if (runtimeConfig.Runtime?.Cache?.Enabled == true)
190+
{
191+
HandleOrRecordException(new DataApiBuilderException(
192+
message: USER_DELEGATED_AUTH_CACHING_ERR_MSG,
193+
statusCode: HttpStatusCode.ServiceUnavailable,
194+
subStatusCode: DataApiBuilderException.SubStatusCodes.ConfigValidationError));
195+
}
196+
}
122197
}
123198

124199
/// <summary>

src/Core/Resolvers/Factories/QueryManagerFactory.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,28 @@ public class QueryManagerFactory : IAbstractQueryManagerFactory
2626
private readonly ILogger<IQueryExecutor> _logger;
2727
private readonly IHttpContextAccessor _contextAccessor;
2828
private readonly HotReloadEventHandler<HotReloadEventArgs>? _handler;
29+
private readonly IOboTokenProvider? _oboTokenProvider;
2930

3031
/// <summary>
3132
/// Initiates an instance of QueryManagerFactory
3233
/// </summary>
3334
/// <param name="runtimeConfigProvider">runtimeconfigprovider.</param>
3435
/// <param name="logger">logger.</param>
3536
/// <param name="contextAccessor">httpcontextaccessor.</param>
37+
/// <param name="oboTokenProvider">Optional OBO token provider for user-delegated authentication.</param>
3638
public QueryManagerFactory(
3739
RuntimeConfigProvider runtimeConfigProvider,
3840
ILogger<IQueryExecutor> logger,
3941
IHttpContextAccessor contextAccessor,
40-
HotReloadEventHandler<HotReloadEventArgs>? handler)
42+
HotReloadEventHandler<HotReloadEventArgs>? handler,
43+
IOboTokenProvider? oboTokenProvider = null)
4144
{
4245
handler?.Subscribe(QUERY_MANAGER_FACTORY_ON_CONFIG_CHANGED, OnConfigChanged);
4346
_handler = handler;
4447
_runtimeConfigProvider = runtimeConfigProvider;
4548
_logger = logger;
4649
_contextAccessor = contextAccessor;
50+
_oboTokenProvider = oboTokenProvider;
4751
_queryBuilders = new Dictionary<DatabaseType, IQueryBuilder>();
4852
_queryExecutors = new Dictionary<DatabaseType, IQueryExecutor>();
4953
_dbExceptionsParsers = new Dictionary<DatabaseType, DbExceptionParser>();
@@ -73,7 +77,7 @@ private void ConfigureQueryManagerFactory()
7377
case DatabaseType.MSSQL:
7478
queryBuilder = new MsSqlQueryBuilder();
7579
exceptionParser = new MsSqlDbExceptionParser(_runtimeConfigProvider);
76-
queryExecutor = new MsSqlQueryExecutor(_runtimeConfigProvider, exceptionParser, _logger, _contextAccessor, _handler);
80+
queryExecutor = new MsSqlQueryExecutor(_runtimeConfigProvider, exceptionParser, _logger, _contextAccessor, _handler, _oboTokenProvider);
7781
break;
7882
case DatabaseType.MySQL:
7983
queryBuilder = new MySqlQueryBuilder();
@@ -88,7 +92,7 @@ private void ConfigureQueryManagerFactory()
8892
case DatabaseType.DWSQL:
8993
queryBuilder = new DwSqlQueryBuilder(enableNto1JoinOpt: _runtimeConfigProvider.GetConfig().EnableDwNto1JoinOpt);
9094
exceptionParser = new MsSqlDbExceptionParser(_runtimeConfigProvider);
91-
queryExecutor = new MsSqlQueryExecutor(_runtimeConfigProvider, exceptionParser, _logger, _contextAccessor, _handler);
95+
queryExecutor = new MsSqlQueryExecutor(_runtimeConfigProvider, exceptionParser, _logger, _contextAccessor, _handler, _oboTokenProvider);
9296
break;
9397
default:
9498
throw new NotSupportedException(dataSource.DatabaseTypeNotSupportedMessage);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Identity.Client;
5+
6+
namespace Azure.DataApiBuilder.Core.Resolvers;
7+
8+
/// <summary>
9+
/// Wrapper interface for MSAL confidential client operations.
10+
/// This abstraction enables unit testing by allowing mocking of MSAL's sealed classes.
11+
/// </summary>
12+
public interface IMsalClientWrapper
13+
{
14+
/// <summary>
15+
/// Acquires a token on behalf of a user using the OBO flow.
16+
/// </summary>
17+
/// <param name="scopes">The scopes to request.</param>
18+
/// <param name="userAssertion">The user assertion (incoming JWT).</param>
19+
/// <param name="cancellationToken">Cancellation token.</param>
20+
/// <returns>The authentication result containing the access token.</returns>
21+
Task<AuthenticationResult> AcquireTokenOnBehalfOfAsync(
22+
string[] scopes,
23+
string userAssertion,
24+
CancellationToken cancellationToken = default);
25+
}

0 commit comments

Comments
 (0)