You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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]>
Copy file name to clipboardExpand all lines: schemas/dab.draft.schema.json
+22Lines changed: 22 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -61,6 +61,28 @@
61
61
"maximum": 2147483647
62
62
}
63
63
}
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.",
"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)."
Copy file name to clipboardExpand all lines: src/Config/DataApiBuilderException.cs
+10-1Lines changed: 10 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -20,6 +20,11 @@ public class DataApiBuilderException : Exception
20
20
publicconststringGRAPHQL_MUTATION_FIELD_AUTHZ_FAILURE="Unauthorized due to one or more fields in this mutation.";
21
21
publicconststringGRAPHQL_GROUPBY_FIELD_AUTHZ_FAILURE="Access forbidden to field '{0}' referenced in the groupBy argument.";
22
22
publicconststringGRAPHQL_AGGREGATION_FIELD_AUTHZ_FAILURE="Access forbidden to field '{0}' referenced in the aggregation function '{1}'.";
23
+
publicconststringOBO_IDENTITY_CLAIMS_MISSING="User-delegated authentication failed: Neither 'oid' nor 'sub' claim found in the access token.";
24
+
publicconststringOBO_TENANT_CLAIM_MISSING="User-delegated authentication failed: 'tid' (tenant id) claim not found in the access token.";
25
+
publicconststringOBO_TOKEN_ACQUISITION_FAILED="User-delegated authentication failed: Unable to acquire database access token on behalf of the user.";
26
+
publicconststringOBO_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
+
publicconststringOBO_MISSING_DATABASE_AUDIENCE="User-delegated authentication failed: 'database-audience' is not configured in the data source's user-delegated-auth settings.";
23
28
24
29
publicenumSubStatusCodes
25
30
{
@@ -127,7 +132,11 @@ public enum SubStatusCodes
127
132
/// <summary>
128
133
/// Error due to client input validation failure.
129
134
/// </summary>
130
-
DatabaseInputError
135
+
DatabaseInputError,
136
+
/// <summary>
137
+
/// User-delegated (OBO) authentication failed due to missing identity claims.
0 commit comments