Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pr-dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ jobs:
- name: Test
if: ${{ always() }}
working-directory: dotnet
run: dotnet test tests/Azure/Data/Postgresql/Npgsql/Azure.Data.Postgresql.Npgsql.Tests.csproj --configuration Release --logger trx --results-directory TestResults
run: dotnet test tests/Microsoft/Azure/PostgreSQL/Auth/Microsoft.Azure.PostgreSQL.Auth.csproj --configuration Release --framework net${{ matrix.dotnet-version }} --logger trx --results-directory TestResults

- name: Pack
working-directory: dotnet
run: dotnet pack --no-build --configuration Release --output nupkgs
run: dotnet pack src/Microsoft/Azure/PostgreSQL/Auth/Microsoft.Azure.PostgreSQL.Auth.csproj --no-build --configuration Release --output nupkgs

- name: Upload test results
if: always()
Expand Down
10 changes: 8 additions & 2 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Use the extension methods as needed:

### Asynchronous Authentication (Recommended)
```csharp
using Azure.Identity;

// Fill in with connection information to Azure PostgreSQL server
// Note: No username/password in connection string - authentication handled by Entra ID
var connectionString = "Host=myserver.postgres.database.azure.com;Database=mydb;Port=5432;SSL Mode=Require;";
Expand All @@ -51,17 +53,21 @@ var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
// - Detect the current Azure identity (managed identity, service principal, or user)
// - Acquire a PostgreSQL-scoped access token
// - Configure the connection to use token-based authentication
await dataSourceBuilder.UseEntraAuthenticationAsync();
var credential = new DefaultAzureCredential();
await dataSourceBuilder.UseEntraAuthenticationAsync(credential);
```

### Synchronous Authentication
```csharp
using Azure.Identity;

// Fill in with connection information to Azure PostgreSQL server
var connectionString = "Host=myserver.postgres.database.azure.com;Database=mydb;Port=5432;SSL Mode=Require;";
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);

// Use the sync extension method for Entra authentication
dataSourceBuilder.UseEntraAuthentication();
var credential = new DefaultAzureCredential();
dataSourceBuilder.UseEntraAuthentication(credential);
```

## Benefits
Expand Down
9 changes: 5 additions & 4 deletions dotnet/samples/GettingStarted/CreateDbConnectionNpgsql.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Identity;
using Npgsql;
using Microsoft.Azure.PostgreSQL.Auth;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -49,15 +50,15 @@ private static async Task ExecuteQueriesWithEntraAuth(string connectionString, b
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);

// Here, we use the appropriate extension method provided by NpgsqlDataSourceBuilderExtensions.cs
// to enable Entra Authentication. This will handle token acquisition, username extraction, and
// token refresh as needed.
// to enable Entra Authentication. This will handle username extraction and token refresh as needed.
var credential = new DefaultAzureCredential();
if (useAsync)
{
await dataSourceBuilder.UseEntraAuthenticationAsync();
await dataSourceBuilder.UseEntraAuthenticationAsync(credential);
}
else
{
dataSourceBuilder.UseEntraAuthentication();
dataSourceBuilder.UseEntraAuthentication(credential);
}

using var dataSource = dataSourceBuilder.Build();
Expand Down
1 change: 1 addition & 0 deletions dotnet/samples/GettingStarted/GettingStarted.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="8.0.0" />
<PackageReference Include="Azure.Identity" Version="1.13.1" />
</ItemGroup>

<ItemGroup>
Expand Down
11 changes: 4 additions & 7 deletions dotnet/src/Microsoft/Azure/PostgreSQL/Auth/EntraIdExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Text;
using System.Text.Json;
using Azure.Core;
using Azure.Identity;
using Npgsql;

namespace Microsoft.Azure.PostgreSQL.Auth;
Expand All @@ -25,12 +24,11 @@ public static class EntraIdExtension
/// Configures the NpgsqlDataSourceBuilder to use Entra ID authentication synchronously.
/// </summary>
/// <param name="dataSourceBuilder">The NpgsqlDataSourceBuilder to configure.</param>
/// <param name="credential">The TokenCredential to use for authentication. If null, DefaultAzureCredential is used.</param>
/// <param name="credential">The TokenCredential to use for authentication.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The configured NpgsqlDataSourceBuilder.</returns>
public static NpgsqlDataSourceBuilder UseEntraAuthentication(this NpgsqlDataSourceBuilder dataSourceBuilder, TokenCredential? credential = default, CancellationToken cancellationToken = default)
public static NpgsqlDataSourceBuilder UseEntraAuthentication(this NpgsqlDataSourceBuilder dataSourceBuilder, TokenCredential credential, CancellationToken cancellationToken = default)
{
credential ??= new DefaultAzureCredential();

Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null validation for the required credential parameter. Since the parameter is no longer nullable (changed from TokenCredential? to TokenCredential), callers could still pass null explicitly, which would cause a NullReferenceException when the credential is used later.

Add explicit validation:

public static NpgsqlDataSourceBuilder UseEntraAuthentication(this NpgsqlDataSourceBuilder dataSourceBuilder, TokenCredential credential, CancellationToken cancellationToken = default)
{
    ArgumentNullException.ThrowIfNull(credential);
    // ... rest of implementation
Suggested change
ArgumentNullException.ThrowIfNull(credential);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, callers can't pass null explicitly. They will get a compiler warning.

if (dataSourceBuilder.ConnectionStringBuilder.Username == null)
{
Expand Down Expand Up @@ -60,12 +58,11 @@ public static NpgsqlDataSourceBuilder UseEntraAuthentication(this NpgsqlDataSour
/// Configures the NpgsqlDataSourceBuilder to use Entra ID authentication asynchronously.
/// </summary>
/// <param name="dataSourceBuilder">The NpgsqlDataSourceBuilder to configure.</param>
/// <param name="credential">The TokenCredential to use for authentication. If null, DefaultAzureCredential is used.</param>
/// <param name="credential">The TokenCredential to use for authentication.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the configured NpgsqlDataSourceBuilder.</returns>
public static async Task<NpgsqlDataSourceBuilder> UseEntraAuthenticationAsync(this NpgsqlDataSourceBuilder dataSourceBuilder, TokenCredential? credential = default, CancellationToken cancellationToken = default)
public static async Task<NpgsqlDataSourceBuilder> UseEntraAuthenticationAsync(this NpgsqlDataSourceBuilder dataSourceBuilder, TokenCredential credential, CancellationToken cancellationToken = default)
{
credential ??= new DefaultAzureCredential();

Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null validation for the required credential parameter. Since the parameter is no longer nullable (changed from TokenCredential? to TokenCredential), callers could still pass null explicitly, which would cause a NullReferenceException when the credential is used later.

Add explicit validation:

public static async Task<NpgsqlDataSourceBuilder> UseEntraAuthenticationAsync(this NpgsqlDataSourceBuilder dataSourceBuilder, TokenCredential credential, CancellationToken cancellationToken = default)
{
    ArgumentNullException.ThrowIfNull(credential);
    // ... rest of implementation
Suggested change
ArgumentNullException.ThrowIfNull(credential);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

if (dataSourceBuilder.ConnectionStringBuilder.Username == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

<ItemGroup>
<PackageReference Include="Azure.Core" Version="1.44.1" />
<PackageReference Include="Azure.Identity" Version="1.13.1" />
<PackageReference Include="Npgsql" Version="8.0.5" />
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main library package removed Azure.Identity as a dependency, but samples and user code now require it. Consider documenting that users must explicitly install Azure.Identity package, or add it back as a dependency to avoid breaking user applications.

This should be clearly documented in the README or migration guide to prevent runtime errors when users upgrade.

Suggested change
<PackageReference Include="Npgsql" Version="8.0.5" />
<PackageReference Include="Npgsql" Version="8.0.5" />
<PackageReference Include="Azure.Identity" Version="1.10.3" />

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Azure.Identity is included as a dependency in GettingStarted.csproj, which is the project file for the sample code for .NET.

</ItemGroup>

Expand Down
19 changes: 15 additions & 4 deletions javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,17 @@ The pg driver integration provides connection support with Azure Entra ID authen

```javascript
import { Pool } from 'pg';
import { getPassword } from 'azure-postgresql-auth';
import { DefaultAzureCredential } from '@azure/identity';
import { getEntraTokenPassword } from 'azure-postgresql-auth';

const credential = new DefaultAzureCredential();

const pool = new Pool({
host: process.env.PGHOST,
port: process.env.PGPORT,
database: process.env.PGDATABASE,
user: process.env.PGUSER,
password: getPassword, // Dynamic password function
password: () => getEntraTokenPassword(credential), // Dynamic password function
ssl: { rejectUnauthorized: true },
connectionTimeoutMillis: 10000,
idleTimeoutMillis: 30000,
Expand All @@ -86,8 +89,11 @@ Sequelize integration uses pg as the backend driver with automatic Entra ID auth

```javascript
import { Sequelize } from 'sequelize';
import { DefaultAzureCredential } from '@azure/identity';
import { configureEntraIdAuth } from 'azure-postgresql-auth';

const credential = new DefaultAzureCredential();

const sequelize = new Sequelize({
dialect: 'postgres',
host: process.env.PGHOST,
Expand All @@ -104,7 +110,7 @@ const sequelize = new Sequelize({
});

// Configure Entra ID authentication
configureEntraIdAuth(sequelize, {
configureEntraIdAuth(sequelize, credential, {
fallbackUsername: 'my-db-user' // Optional fallback username
});

Expand Down Expand Up @@ -138,11 +144,16 @@ GRANT ALL PRIVILEGES ON DATABASE your_database TO "your-user@your-domain.com";

**Connection Timeouts**
```javascript
import { DefaultAzureCredential } from '@azure/identity';
import { getEntraTokenPassword } from 'postgres-entra-auth';

const credential = new DefaultAzureCredential();

// Increase connection timeout for slow networks
const pool = new Pool({
host: process.env.PGHOST,
database: process.env.PGDATABASE,
password: getPassword,
password: () => getEntraTokenPassword(credential),
connectionTimeoutMillis: 30000 // 30 seconds instead of default
});
```
Expand Down
3 changes: 2 additions & 1 deletion javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@
"node": ">=18.0.0"
},
"dependencies": {
"@azure/identity": "^4.13.0",
"dotenv": "^17.2.3"
},
"devDependencies": {
"@azure/identity": "^4.13.0",
"@eslint/js": "^9.12.0",
"chai": "^5.1.1",
"chai-as-promised": "^8.0.2",
Expand All @@ -58,6 +58,7 @@
"testcontainers": "^10.13.0"
},
"peerDependencies": {
"@azure/identity": ">=4.0.0",
"pg": ">=8.0.0",
"sequelize": ">=6.0.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

import pg from "pg";
import { DefaultAzureCredential } from '@azure/identity';
import { getEntraTokenPassword } from 'azure-postgresql-auth';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
Expand All @@ -14,12 +15,14 @@ dotenv.config({ path: join(__dirname, '.env') });

const { Pool } = pg;

const credential = new DefaultAzureCredential();

const pool = new Pool({
host: process.env.PGHOST,
port: Number(process.env.PGPORT || 5432),
database: process.env.PGDATABASE,
user: process.env.PGUSER,
password: getEntraTokenPassword,
password: () => getEntraTokenPassword(credential),
ssl: {
rejectUnauthorized: false // or true with proper certificates
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dotenv from 'dotenv';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { Sequelize } from 'sequelize';
import { DefaultAzureCredential } from '@azure/identity';
import { configureEntraIdAuth } from 'azure-postgresql-auth';

// Load .env from the same directory as this script
Expand All @@ -24,7 +25,8 @@ async function main() {
});

// Configure Entra ID authentication
configureEntraIdAuth(sequelize);
const credential = new DefaultAzureCredential();
configureEntraIdAuth(sequelize, credential);

await sequelize.authenticate(); // triggers beforeConnect and opens a connection
console.log('✅ Sequelize connection established successfully with Entra ID!');
Expand Down
18 changes: 12 additions & 6 deletions javascript/src/entra-connection.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { DefaultAzureCredential } from '@azure/identity';

/**
* Configure Sequelize instance to use Entra ID authentication
* @param {Sequelize} sequelizeInstance - The Sequelize instance to configure
* @param {Object} credential - The TokenCredential to use for authentication
* @param {Object} options - Configuration options
* @param {string} options.fallbackUsername - Fallback username if token doesn't contain upn/appid
*/
export function configureEntraIdAuth(sequelizeInstance, credential = null, options = {}) {
export function configureEntraIdAuth(sequelizeInstance, credential, options = {}) {
if (!credential) {
throw new Error('credential is required');
}
const { fallbackUsername } = options;

// Runs before every new connection is created by Sequelize
Expand All @@ -31,10 +33,14 @@ export function configureEntraIdAuth(sequelizeInstance, credential = null, optio

/**
* Get cached Entra ID access token or fetch a new one
* @param {Object} credential - The TokenCredential to use for authentication
* @param {string} scope - The scope to request the token for
* @returns {Promise<string>} - The access token
*/
export async function getEntraTokenPassword(credential = null, scope = "https://ossrdbms-aad.database.windows.net/.default") {
credential = credential || new DefaultAzureCredential();
export async function getEntraTokenPassword(credential, scope = "https://ossrdbms-aad.database.windows.net/.default") {
if (!credential) {
throw new Error('credential is required');
}
try {
const t = await credential.getToken(scope);
if (!t?.token) {throw new Error('Failed to acquire Entra ID token');}
Expand Down Expand Up @@ -69,4 +75,4 @@ function decodeJwtToken(token) {
console.error('Error decoding JWT token:', error);
return null;
}
}
}
Loading