├── python
├── .gitignore
├── requirements.txt
├── psycopg2
│ ├── errors.py
│ ├── sample.py
│ ├── entra_connection.py
│ └── shared.py
├── psycopg3
│ ├── errors.py
│ ├── entra_connection.py
│ ├── async_entra_connection.py
│ ├── sample.py
│ └── shared.py
└── sqlalchemy
│ ├── errors.py
│ ├── entra_connection.py
│ ├── async_entra_connection.py
│ ├── sample.py
│ └── shared.py
├── dotnet
├── appsettings.example.json
├── .gitignore
├── Constants.cs
├── PGEntraExamples.csproj
├── sample.cs
└── NpgsqlDataSourceBuilderExtensions.cs
├── javascript
├── package.json
├── entra-connection.js
├── sequelize-sample.js
├── pg-sample.js
└── package-lock.json
├── java
├── pom.xml
├── EntraIdExtensionJdbc.java
└── EntraIdExtensionHibernate.java
└── README.md
/python/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | __pycache__/
3 | venv
--------------------------------------------------------------------------------
/dotnet/appsettings.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "Host": "your-postgres-server.postgres.database.azure.com",
3 | "Database": "mydatabase",
4 | "Port": 5432,
5 | "SslMode": "Require"
6 | }
--------------------------------------------------------------------------------
/python/requirements.txt:
--------------------------------------------------------------------------------
1 | azure-identity # also installs azure-core as a dependency
2 | psycopg-pool
3 | asyncio
4 | dotenv
5 | psycopg[binary,pool]
6 | psycopg2-binary
7 | aiohttp
8 | sqlalchemy
--------------------------------------------------------------------------------
/dotnet/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from `dotnet new gitignore`
5 |
6 | appsettings.json
7 |
8 | # dotenv files
9 | .env
10 | bin/
11 | obj/
12 |
--------------------------------------------------------------------------------
/javascript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "postgres-entra-sample",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "description": "PostgreSQL connection sample with Entra ID authentication",
6 | "scripts": {
7 | "pg": "node pg-sample.js",
8 | "sequelize": "node sequelize-sample.js"
9 | },
10 | "dependencies": {
11 | "pg": "^8.11.3",
12 | "sequelize": "^6.35.2",
13 | "@azure/identity": "^4.0.0",
14 | "dotenv": "^16.4.5"
15 | }
16 | }
--------------------------------------------------------------------------------
/dotnet/Constants.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft. All rights reserved.
2 |
3 | namespace Postgres.EntraAuth;
4 |
5 | ///
6 | /// Constants for AzureDBForPostgres.
7 | ///
8 | public static class Constants
9 | {
10 | ///
11 | /// The scope for the AzureDBForPostgres service, to be used with Entra.
12 | ///
13 | public const string AzureDBForPostgresScope = "https://ossrdbms-aad.database.windows.net/.default";
14 | public const string AzureManagementScope = "https://management.azure.com/.default";
15 | }
--------------------------------------------------------------------------------
/python/psycopg2/errors.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | class AzurePgEntraError(Exception):
4 | """Base class for all custom exceptions in the project."""
5 |
6 | pass
7 |
8 |
9 | class TokenDecodeError(AzurePgEntraError):
10 | """Raised when a token value is invalid."""
11 |
12 | pass
13 |
14 |
15 | class UsernameExtractionError(AzurePgEntraError):
16 | """Raised when username cannot be extracted from token."""
17 |
18 | pass
19 |
20 |
21 | class CredentialValueError(AzurePgEntraError):
22 | """Raised when token credential is invalid."""
23 |
24 | pass
25 |
26 |
27 | class EntraConnectionValueError(AzurePgEntraError):
28 | """Raised when Entra connection credentials are invalid."""
29 |
30 | pass
31 |
32 |
33 | class ScopePermissionError(AzurePgEntraError):
34 | """Raised when the provided scope does not have sufficient permissions."""
35 |
36 | pass
37 |
--------------------------------------------------------------------------------
/python/psycopg3/errors.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | class AzurePgEntraError(Exception):
4 | """Base class for all custom exceptions in the project."""
5 |
6 | pass
7 |
8 |
9 | class TokenDecodeError(AzurePgEntraError):
10 | """Raised when a token value is invalid."""
11 |
12 | pass
13 |
14 |
15 | class UsernameExtractionError(AzurePgEntraError):
16 | """Raised when username cannot be extracted from token."""
17 |
18 | pass
19 |
20 |
21 | class CredentialValueError(AzurePgEntraError):
22 | """Raised when token credential is invalid."""
23 |
24 | pass
25 |
26 |
27 | class EntraConnectionValueError(AzurePgEntraError):
28 | """Raised when Entra connection credentials are invalid."""
29 |
30 | pass
31 |
32 |
33 | class ScopePermissionError(AzurePgEntraError):
34 | """Raised when the provided scope does not have sufficient permissions."""
35 |
36 | pass
37 |
--------------------------------------------------------------------------------
/python/sqlalchemy/errors.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | class AzurePgEntraError(Exception):
4 | """Base class for all custom exceptions in the project."""
5 |
6 | pass
7 |
8 |
9 | class TokenDecodeError(AzurePgEntraError):
10 | """Raised when a token value is invalid."""
11 |
12 | pass
13 |
14 |
15 | class UsernameExtractionError(AzurePgEntraError):
16 | """Raised when username cannot be extracted from token."""
17 |
18 | pass
19 |
20 |
21 | class CredentialValueError(AzurePgEntraError):
22 | """Raised when token credential is invalid."""
23 |
24 | pass
25 |
26 |
27 | class EntraConnectionValueError(AzurePgEntraError):
28 | """Raised when Entra connection credentials are invalid."""
29 |
30 | pass
31 |
32 |
33 | class ScopePermissionError(AzurePgEntraError):
34 | """Raised when the provided scope does not have sufficient permissions."""
35 |
36 | pass
37 |
--------------------------------------------------------------------------------
/dotnet/PGEntraExamples.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0;net9.0
6 |
7 | $(TargetFrameworks);net10.0
8 | net9.0
9 | enable
10 | enable
11 |
12 | $(NoWarn);SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,SKEXP0120
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/python/psycopg2/sample.py:
--------------------------------------------------------------------------------
1 | """
2 | Sample demonstrating psycopg2 connection with synchronous Entra ID authentication for Azure PostgreSQL.
3 | """
4 |
5 | import os
6 |
7 | from dotenv import load_dotenv
8 | from psycopg2 import pool
9 | from entra_connection import EntraConnection
10 |
11 | # Load environment variables from .env file
12 | load_dotenv()
13 | hostname = os.getenv("HOSTNAME")
14 | database = os.getenv("DATABASE", "postgres")
15 |
16 | def main() -> None:
17 | # We use the EntraConnection class to enable synchronous Entra-based authentication for database access.
18 | # This class is applied whenever the connection pool creates a new connection, ensuring that Entra
19 | # authentication tokens are properly managed and refreshed so that each connection uses a valid token.
20 | #
21 | # For more details, see: https://www.psycopg.org/docs/advanced.html#subclassing-connection
22 | connection_pool = pool.ThreadedConnectionPool(
23 | minconn=1,
24 | maxconn=5,
25 | host=hostname,
26 | database=database,
27 | connection_factory=EntraConnection,
28 | )
29 |
30 | conn = connection_pool.getconn()
31 | try:
32 | with conn.cursor() as cur:
33 | cur.execute("SELECT now()")
34 | result = cur.fetchone()
35 | print(f"Database time: {result[0]}")
36 | finally:
37 | connection_pool.putconn(conn)
38 | connection_pool.closeall()
39 |
40 |
41 | if __name__ == "__main__":
42 | main()
43 |
--------------------------------------------------------------------------------
/java/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.azure.samples
8 | postgres-entra-samples
9 | 1.0-SNAPSHOT
10 |
11 |
12 | UTF-8
13 | 17
14 | 17
15 |
16 |
17 |
18 |
19 |
20 | org.postgresql
21 | postgresql
22 | 42.7.4
23 |
24 |
25 |
26 |
27 | com.azure
28 | azure-identity-extensions
29 | 1.2.0
30 |
31 |
32 |
33 |
34 | org.hibernate
35 | hibernate-core
36 | 6.4.4.Final
37 |
38 |
39 |
40 |
41 | com.zaxxer
42 | HikariCP
43 | 5.1.0
44 |
45 |
46 |
47 |
48 | .
49 |
50 |
51 | .
52 |
53 | application.properties
54 |
55 |
56 |
57 |
58 |
59 | org.codehaus.mojo
60 | exec-maven-plugin
61 | 3.1.0
62 |
63 | EntraIdExtensionJdbc
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/python/sqlalchemy/entra_connection.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | from typing import Any
4 |
5 | from azure.core.credentials import TokenCredential
6 |
7 | try:
8 | from sqlalchemy import Engine, event
9 | from sqlalchemy.engine import Dialect
10 | except ImportError as e:
11 | raise ImportError(
12 | "SQLAlchemy dependencies are not installed. "
13 | "Install them with: pip install azurepg-entra[sqlalchemy]"
14 | ) from e
15 |
16 | from shared import get_entra_conninfo
17 | from errors import (
18 | CredentialValueError,
19 | EntraConnectionValueError,
20 | )
21 |
22 |
23 | def enable_entra_authentication(engine: Engine) -> None:
24 | """
25 | Enable Azure Entra ID authentication for a SQLAlchemy engine.
26 |
27 | This function registers an event listener that automatically provides
28 | Entra ID credentials for each database connection if they are not already set.
29 |
30 | Args:
31 | engine: The SQLAlchemy Engine to enable Entra authentication for
32 | """
33 |
34 | @event.listens_for(engine, "do_connect")
35 | def provide_token(
36 | dialect: Dialect, conn_rec: Any, cargs: Any, cparams: dict[str, Any]
37 | ) -> None:
38 | """Event handler that provides Entra credentials for each connection.
39 |
40 | Raises:
41 | CredentialValueError: If the provided credential is not a valid TokenCredential.
42 | EntraConnectionValueError: If Entra connection credentials cannot be retrieved
43 | """
44 | credential = cparams.get("credential", None)
45 | if credential and not isinstance(credential, (TokenCredential)):
46 | raise CredentialValueError(
47 | "credential must be a TokenCredential for sync connections"
48 | )
49 | # Check if credentials are already present
50 | has_user = "user" in cparams
51 | has_password = "password" in cparams
52 |
53 | # Only get Entra credentials if user or password is missing
54 | if not has_user or not has_password:
55 | try:
56 | entra_creds = get_entra_conninfo(credential)
57 | except Exception as e:
58 | raise EntraConnectionValueError(
59 | "Could not retrieve Entra credentials"
60 | ) from e
61 | # Only update missing credentials
62 | if not has_user and "user" in entra_creds:
63 | cparams["user"] = entra_creds["user"]
64 | if not has_password and "password" in entra_creds:
65 | cparams["password"] = entra_creds["password"]
66 |
--------------------------------------------------------------------------------
/python/sqlalchemy/async_entra_connection.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | from typing import Any
4 |
5 | from azure.core.credentials import TokenCredential
6 |
7 | try:
8 | from sqlalchemy import event
9 | from sqlalchemy.engine import Dialect
10 | from sqlalchemy.ext.asyncio import AsyncEngine
11 | except ImportError as e:
12 | # Provide a helpful error message if SQLAlchemy dependencies are missing
13 | raise ImportError(
14 | "SQLAlchemy dependencies are not installed. "
15 | "Install them with: pip install azurepg-entra[sqlalchemy]"
16 | ) from e
17 |
18 | from shared import get_entra_conninfo
19 | from errors import (
20 | CredentialValueError,
21 | EntraConnectionValueError,
22 | )
23 |
24 |
25 | def enable_entra_authentication_async(engine: AsyncEngine) -> None:
26 | """
27 | Enable Azure Entra ID authentication for an async SQLAlchemy engine.
28 |
29 | This function registers an event listener that automatically provides
30 | Entra ID credentials for each database connection if they are not already set.
31 | Event handlers do not support async behavior so the token fetching will still
32 | be synchronous.
33 |
34 | Args:
35 | engine: The async SQLAlchemy Engine to enable Entra authentication for
36 | """
37 |
38 | @event.listens_for(engine.sync_engine, "do_connect")
39 | def provide_token(
40 | dialect: Dialect, conn_rec: Any, cargs: Any, cparams: dict[str, Any]
41 | ) -> None:
42 | """Event handler that provides Entra credentials for each sync connection.
43 |
44 | Raises:
45 | CredentialValueError: If the provided credential is not a valid TokenCredential.
46 | EntraConnectionValueError: If Entra connection credentials cannot be retrieved
47 | """
48 | credential = cparams.get("credential", None)
49 | if credential and not isinstance(credential, (TokenCredential)):
50 | raise CredentialValueError(
51 | "credential must be a TokenCredential for async connections"
52 | )
53 | # Check if credentials are already present
54 | has_user = "user" in cparams
55 | has_password = "password" in cparams
56 |
57 | # Only get Entra credentials if user or password is missing
58 | if not has_user or not has_password:
59 | try:
60 | entra_creds = get_entra_conninfo(credential)
61 | except Exception as e:
62 | raise EntraConnectionValueError(
63 | "Could not retrieve Entra credentials"
64 | ) from e
65 | # Only update missing credentials
66 | if not has_user and "user" in entra_creds:
67 | cparams["user"] = entra_creds["user"]
68 | if not has_password and "password" in entra_creds:
69 | cparams["password"] = entra_creds["password"]
70 |
--------------------------------------------------------------------------------
/javascript/entra-connection.js:
--------------------------------------------------------------------------------
1 | import { DefaultAzureCredential } from '@azure/identity';
2 |
3 | /**
4 | * Configure Sequelize instance to use Entra ID authentication
5 | * @param {Sequelize} sequelizeInstance - The Sequelize instance to configure
6 | * @param {Object} options - Configuration options
7 | * @param {string} options.fallbackUsername - Fallback username if token doesn't contain upn/appid
8 | */
9 | export function configureEntraIdAuth(sequelizeInstance, credential = null, options = {}) {
10 | const { fallbackUsername } = options;
11 |
12 | // Runs before every new connection is created by Sequelize
13 | sequelizeInstance.beforeConnect(async (config) => {
14 | console.log("Fetching Entra ID access token...");
15 | const token = await getEntraTokenPassword(credential, "https://ossrdbms-aad.database.windows.net/.default");
16 |
17 | // Derive username from token if you want (optional):
18 | const claims = decodeJwtToken(token);
19 | const derivedUser = claims.upn || claims.appid || fallbackUsername || process.env.PGUSER;
20 | if (!derivedUser) {throw new Error("Could not determine DB username");}
21 |
22 | config.username = derivedUser; // must match an AAD-mapped role in Postgres
23 | config.password = token; // raw token, no "Bearer "
24 | });
25 |
26 | return sequelizeInstance;
27 | }
28 |
29 | /**
30 | * Get cached Entra ID access token or fetch a new one
31 | * @returns {Promise} - The access token
32 | */
33 | export async function getEntraTokenPassword(credential = null, scope = "https://ossrdbms-aad.database.windows.net/.default") {
34 | credential = credential || new DefaultAzureCredential();
35 | try {
36 | const t = await credential.getToken(scope);
37 | if (!t?.token) {throw new Error('Failed to acquire Entra ID token');}
38 | return t.token;
39 | } catch (error) {
40 | console.error('❌ Token acquisition failed:', error.message);
41 | throw error;
42 | }
43 | }
44 |
45 | /**
46 | * Decode JWT token to extract user information
47 | * @param {string} token - The JWT access token
48 | * @returns {object} - Decoded token payload
49 | */
50 | function decodeJwtToken(token) {
51 | try {
52 | // JWT tokens have 3 parts separated by dots: header.payload.signature
53 | const parts = token.split('.');
54 | if (parts.length !== 3) {
55 | throw new Error('Invalid JWT token format');
56 | }
57 |
58 | // Decode the payload (second part)
59 | const payload = parts[1];
60 | // Add padding if needed for base64 decoding
61 | const paddedPayload = payload + '='.repeat((4 - payload.length % 4) % 4);
62 | const decodedPayload = Buffer.from(paddedPayload, 'base64url').toString('utf8');
63 |
64 | return JSON.parse(decodedPayload);
65 | } catch (error) {
66 | console.error('Error decoding JWT token:', error);
67 | return null;
68 | }
69 | }
--------------------------------------------------------------------------------
/python/psycopg3/entra_connection.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | from typing import Any
4 |
5 | from azure.core.credentials import TokenCredential
6 |
7 | try:
8 | from psycopg import Connection
9 | except ImportError as e:
10 | raise ImportError(
11 | "psycopg3 dependencies are not installed. "
12 | "Install them with: pip install azurepg-entra[psycopg3]"
13 | ) from e
14 |
15 | from shared import get_entra_conninfo
16 | from errors import (
17 | CredentialValueError,
18 | EntraConnectionValueError,
19 | )
20 |
21 |
22 | class EntraConnection(Connection):
23 | """Synchronous connection class for using Entra authentication with Azure PostgreSQL."""
24 |
25 | @classmethod
26 | def connect(cls, *args: Any, **kwargs: Any) -> "EntraConnection":
27 | """Establishes a synchronous PostgreSQL connection using Entra authentication.
28 |
29 | This method automatically acquires Azure Entra ID credentials when user or password
30 | are not provided in the connection parameters. If authentication fails, the original
31 | exception is re-raised to the caller.
32 |
33 | Parameters:
34 | *args: Positional arguments to be forwarded to the parent connection method.
35 | **kwargs: Keyword arguments including:
36 | - credential (TokenCredential, optional): Azure credential for token acquisition.
37 | - user (str, optional): Database username. If not provided, extracted from Entra token.
38 | - password (str, optional): Database password. If not provided, uses Entra access token.
39 |
40 | Returns:
41 | EntraConnection: An open synchronous connection to the PostgreSQL database.
42 |
43 | Raises:
44 | CredentialValueError: If the provided credential is not a valid TokenCredential.
45 | EntraConnectionValueError: If Entra connection credentials cannot be retrieved
46 | """
47 | credential = kwargs.pop("credential", None)
48 | if credential and not isinstance(credential, (TokenCredential)):
49 | raise CredentialValueError(
50 | "credential must be a TokenCredential for sync connections"
51 | )
52 |
53 | # Check if we need to acquire Entra authentication info
54 | if not kwargs.get("user") or not kwargs.get("password"):
55 | try:
56 | entra_conninfo = get_entra_conninfo(credential)
57 | except Exception as e:
58 | raise EntraConnectionValueError(
59 | "Could not retrieve Entra credentials"
60 | ) from e
61 | # Always use the token password when Entra authentication is needed
62 | kwargs["password"] = entra_conninfo["password"]
63 | if not kwargs.get("user"):
64 | # If user isn't already set, use the username from the token
65 | kwargs["user"] = entra_conninfo["user"]
66 | return super().connect(*args, **kwargs)
67 |
--------------------------------------------------------------------------------
/python/psycopg3/async_entra_connection.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | from typing import Any
4 |
5 | from azure.core.credentials_async import AsyncTokenCredential
6 |
7 | try:
8 | from psycopg import AsyncConnection
9 | except ImportError as e:
10 | raise ImportError(
11 | "psycopg3 dependencies are not installed. "
12 | "Install them with: pip install azurepg-entra[psycopg3]"
13 | ) from e
14 |
15 | from shared import get_entra_conninfo_async
16 | from errors import (
17 | CredentialValueError,
18 | EntraConnectionValueError,
19 | )
20 |
21 |
22 | class AsyncEntraConnection(AsyncConnection):
23 | """Asynchronous connection class for using Entra authentication with Azure PostgreSQL."""
24 |
25 | @classmethod
26 | async def connect(cls, *args: Any, **kwargs: Any) -> "AsyncEntraConnection":
27 | """Establishes an asynchronous PostgreSQL connection using Entra authentication.
28 |
29 | This method automatically acquires Azure Entra ID credentials when user or password
30 | are not provided in the connection parameters. Authentication errors are printed to
31 | console for debugging purposes.
32 |
33 | Parameters:
34 | *args: Positional arguments to be forwarded to the parent connection method.
35 | **kwargs: Keyword arguments including:
36 | - credential (AsyncTokenCredential, optional): Async Azure credential for token acquisition.
37 | - user (str, optional): Database username. If not provided, extracted from Entra token.
38 | - password (str, optional): Database password. If not provided, uses Entra access token.
39 |
40 | Returns:
41 | AsyncEntraConnection: An open asynchronous connection to the PostgreSQL database.
42 |
43 | Raises:
44 | CredentialValueError: If the provided credential is not a valid AsyncTokenCredential.
45 | EntraConnectionValueError: If Entra connection credentials are invalid.
46 | """
47 | credential = kwargs.pop("credential", None)
48 | if credential and not isinstance(credential, (AsyncTokenCredential)):
49 | raise CredentialValueError(
50 | "credential must be an AsyncTokenCredential for async connections"
51 | )
52 |
53 | # Check if we need to acquire Entra authentication info
54 | if not kwargs.get("user") or not kwargs.get("password"):
55 | try:
56 | entra_conninfo = await get_entra_conninfo_async(credential)
57 | except Exception as e:
58 | raise EntraConnectionValueError(
59 | "Could not retrieve Entra credentials"
60 | ) from e
61 | # Always use the token password when Entra authentication is needed
62 | kwargs["password"] = entra_conninfo["password"]
63 | if not kwargs.get("user"):
64 | # If user isn't already set, use the username from the token
65 | kwargs["user"] = entra_conninfo["user"]
66 | return await super().connect(*args, **kwargs)
67 |
--------------------------------------------------------------------------------
/javascript/sequelize-sample.js:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | import { join, dirname } from 'path';
3 | import { fileURLToPath } from 'url';
4 | import { Sequelize } from 'sequelize';
5 | import { configureEntraIdAuth } from './entra-connection.js';
6 |
7 | // Load .env from the same directory as this script
8 | dotenv.config({ path: join(dirname(fileURLToPath(import.meta.url)), '.env') });
9 |
10 | async function main() {
11 | let sequelize;
12 |
13 | try {
14 | sequelize = new Sequelize({
15 | dialect: 'postgres',
16 | host: process.env.PGHOST,
17 | port: Number(process.env.PGPORT || 5432),
18 | database: process.env.PGDATABASE,
19 | dialectOptions: { ssl: { rejectUnauthorized: true } },
20 | pool: { min: 4, max: 10, idle: 30_000 }
21 | });
22 |
23 | // Configure Entra ID authentication
24 | configureEntraIdAuth(sequelize);
25 |
26 | await sequelize.authenticate(); // triggers beforeConnect and opens a connection
27 | console.log('✅ Sequelize connection established successfully with Entra ID!');
28 |
29 | console.log('🔄 Testing concurrent queries (automatic pooling)...\n');
30 |
31 | async function runConcurrentQuery(queryId) {
32 | const startTime = Date.now();
33 | console.log(`Query ${queryId}: Starting...`);
34 |
35 | const [results] = await sequelize.query(`
36 | SELECT
37 | ${queryId} as query_id,
38 | pg_backend_pid() as backend_pid,
39 | current_user,
40 | now() as query_time,
41 | pg_sleep(2) -- Simulate slow query
42 | `);
43 |
44 | const duration = Date.now() - startTime;
45 | console.log(`Query ${queryId}: Completed in ${duration}ms - Backend PID: ${results[0].backend_pid}`);
46 | return results[0];
47 | }
48 |
49 | const concurrentResults = await Promise.all([
50 | runConcurrentQuery(1),
51 | runConcurrentQuery(2),
52 | runConcurrentQuery(3),
53 | runConcurrentQuery(4),
54 | runConcurrentQuery(5)
55 | ]);
56 |
57 | // Analyze connection reuse
58 | const uniquePids = new Set(concurrentResults.map(r => r.backend_pid));
59 | console.log(`\n📊 Concurrent Query Results:`);
60 | console.log(` Total queries: ${concurrentResults.length}`);
61 | console.log(` Unique connections used: ${uniquePids.size}`);
62 | console.log(` Connection reuse: ${uniquePids.size < concurrentResults.length ? 'YES' : 'NO'}`);
63 |
64 | } catch (error) {
65 | console.error('❌ Error:', error.message);
66 | process.exit(1);
67 | } finally {
68 | if (sequelize) {
69 | try {
70 | await sequelize.close();
71 | console.log('\n🔌 All database connections closed. Program exiting...');
72 | } catch (closeError) {
73 | console.error('⚠️ Error closing connections:', closeError.message);
74 | }
75 | }
76 | }
77 | }
78 |
79 | // Run the main function and handle any unhandled errors
80 | main().catch((error) => {
81 | console.error('💥 Unhandled error in main function:', error);
82 | process.exit(1);
83 | });
84 |
--------------------------------------------------------------------------------
/javascript/pg-sample.js:
--------------------------------------------------------------------------------
1 | import pg from "pg";
2 | import { getEntraTokenPassword } from './entra-connection.js';
3 | import dotenv from 'dotenv';
4 | import { fileURLToPath } from 'url';
5 | import { dirname, join } from 'path';
6 |
7 | // Load .env file from the same directory as this script
8 | const __filename = fileURLToPath(import.meta.url);
9 | const __dirname = dirname(__filename);
10 | dotenv.config({ path: join(__dirname, '.env') });
11 |
12 | const { Pool } = pg;
13 |
14 | const pool = new Pool({
15 | host: process.env.PGHOST,
16 | port: Number(process.env.PGPORT || 5432),
17 | database: process.env.PGDATABASE,
18 | user: process.env.PGUSER,
19 | password: getEntraTokenPassword,
20 | ssl: {
21 | rejectUnauthorized: false // or true with proper certificates
22 | },
23 | connectionTimeoutMillis: 20000,
24 | idleTimeoutMillis: 30000,
25 | });
26 |
27 | async function main() {
28 | try {
29 | console.log('Testing multiple connections from the pool...\n');
30 |
31 | // Function to simulate a database operation
32 | async function performQuery(connectionNumber) {
33 | const client = await pool.connect();
34 | try {
35 | console.log(`Connection ${connectionNumber}: Acquired from pool`);
36 |
37 | const { rows } = await client.query(`
38 | SELECT
39 | current_user,
40 | now() as server_time,
41 | pg_backend_pid() as backend_pid,
42 | '${connectionNumber}' as connection_number
43 | `);
44 |
45 | console.log(`Connection ${connectionNumber} result:`, rows[0]);
46 |
47 | // Simulate some work with a small delay
48 | await new Promise(resolve => setTimeout(resolve, 100));
49 |
50 | return rows[0];
51 | } finally {
52 | console.log(`Connection ${connectionNumber}: Released back to pool`);
53 | client.release();
54 | }
55 | }
56 |
57 | // Test getting multiple connections simultaneously
58 | const connectionPromises = [];
59 | for (let i = 1; i <= 5; i++) {
60 | connectionPromises.push(performQuery(i));
61 | }
62 |
63 | const results = await Promise.all(connectionPromises);
64 |
65 | console.log('\n=== Summary ===');
66 | console.log('All connections completed successfully!');
67 | console.log('Backend PIDs used:', results.map(r => r.backend_pid));
68 |
69 | // Check if different backend PIDs were used (indicating different physical connections)
70 | const uniquePids = new Set(results.map(r => r.backend_pid));
71 | console.log(`Used ${uniquePids.size} unique database connections out of ${results.length} requests`);
72 |
73 | } catch (error) {
74 | console.error('❌ Error:', error.message);
75 | process.exit(1);
76 | } finally {
77 | try {
78 | await pool.end();
79 | console.log('\nPool closed.');
80 | } catch (closeError) {
81 | console.error('⚠️ Error closing pool:', closeError.message);
82 | }
83 | }
84 | }
85 |
86 | // Run the main function and handle any unhandled errors
87 | main().catch((error) => {
88 | console.error('💥 Unhandled error in main function:', error);
89 | process.exit(1);
90 | });
91 |
92 |
--------------------------------------------------------------------------------
/python/psycopg2/entra_connection.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | from typing import Any
4 |
5 | from azure.core.credentials import TokenCredential
6 |
7 | from shared import get_entra_conninfo
8 | from errors import (
9 | CredentialValueError,
10 | EntraConnectionValueError,
11 | )
12 |
13 | try:
14 | from psycopg2.extensions import connection, make_dsn, parse_dsn
15 | except ImportError as e:
16 | # Provide a helpful error message if psycopg2 dependencies are missing
17 | raise ImportError(
18 | "psycopg2 dependencies are not installed. "
19 | "Install them with: pip install azurepg-entra[psycopg2]"
20 | ) from e
21 |
22 | class EntraConnection(connection):
23 | """Establishes a synchronous PostgreSQL connection using Entra authentication.
24 |
25 | This connection class automatically acquires Azure Entra ID credentials when user
26 | or password are not provided in the DSN or connection parameters. Authentication
27 | errors are printed to console for debugging purposes.
28 |
29 | Parameters:
30 | dsn (str): PostgreSQL connection string (Data Source Name).
31 | **kwargs: Additional keyword arguments including:
32 | - credential (TokenCredential, optional): Azure credential for token acquisition.
33 | If None, DefaultAzureCredential() is used.
34 | - user (str, optional): Database username. If not provided, extracted from Entra token.
35 | - password (str, optional): Database password. If not provided, uses Entra access token.
36 |
37 | Raises:
38 | CredentialValueError: If the provided credential is not a valid TokenCredential.
39 | EntraConnectionValueError: If Entra connection credentials cannot be retrieved
40 | """
41 |
42 | def __init__(self, dsn: str, **kwargs: Any) -> None:
43 | # Extract current DSN params
44 | dsn_params = parse_dsn(dsn) if dsn else {}
45 |
46 | credential = kwargs.pop("credential", None)
47 | if credential and not isinstance(credential, (TokenCredential)):
48 | raise CredentialValueError(
49 | "credential must be a TokenCredential for sync connections"
50 | )
51 |
52 | # Check if user and password are already provided
53 | has_user = "user" in dsn_params or "user" in kwargs
54 | has_password = "password" in dsn_params or "password" in kwargs
55 |
56 | # Only get Entra credentials if user or password is missing
57 | if not has_user or not has_password:
58 | try:
59 | entra_creds = get_entra_conninfo(credential)
60 | except (Exception) as e:
61 | raise EntraConnectionValueError(
62 | "Could not retrieve Entra credentials"
63 | ) from e
64 |
65 | # Only update missing credentials
66 | if not has_user and "user" in entra_creds:
67 | dsn_params["user"] = entra_creds["user"]
68 | if not has_password and "password" in entra_creds:
69 | dsn_params["password"] = entra_creds["password"]
70 |
71 | # Update DSN params with any kwargs (kwargs take precedence)
72 | dsn_params.update(kwargs)
73 |
74 | # Create new DSN with updated credentials
75 | new_dsn = make_dsn(**dsn_params)
76 |
77 | # Call parent constructor with updated DSN only
78 | super().__init__(new_dsn)
79 |
--------------------------------------------------------------------------------
/dotnet/sample.cs:
--------------------------------------------------------------------------------
1 | using Azure.Identity;
2 | using Microsoft.Extensions.Configuration;
3 | using Npgsql;
4 | using Postgres.EntraAuth;
5 |
6 | ///
7 | /// This example enables Entra authentication before connecting to the database via NpgsqlConnection.
8 | ///
9 | public class Sample
10 | {
11 | private static IConfiguration _configuration = null!;
12 |
13 | static async Task Main(string[] args)
14 | {
15 | // Build configuration from appsettings.json and environment variables
16 | _configuration = new ConfigurationBuilder()
17 | .SetBasePath(Directory.GetCurrentDirectory())
18 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
19 | .AddEnvironmentVariables()
20 | .Build();
21 | var connectionString = BuildConnectionString();
22 |
23 | Console.WriteLine("Testing Entra Authentication Methods");
24 | Console.WriteLine("=====================================");
25 |
26 | // Test synchronous method
27 | Console.WriteLine("\n1. Testing UseEntraAuthentication (Synchronous):");
28 | await ExecuteQueriesWithEntraAuth(connectionString, useAsync: false);
29 |
30 | // Test asynchronous method
31 | Console.WriteLine("\n2. Testing UseEntraAuthenticationAsync (Asynchronous):");
32 | await ExecuteQueriesWithEntraAuth(connectionString, useAsync: true);
33 |
34 | Console.WriteLine("\nAll tests completed.");
35 | }
36 |
37 | ///
38 | /// Show how to create a connection to the database with Entra authentication and execute some prompts.
39 | ///
40 | /// The PostgreSQL connection string
41 | /// If true, uses UseEntraAuthenticationAsync; otherwise uses UseEntraAuthentication
42 | private static async Task ExecuteQueriesWithEntraAuth(string connectionString, bool useAsync = false)
43 | {
44 |
45 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
46 |
47 | // Here, we use the appropriate extension method provided by NpgsqlDataSourceBuilderExtensions.cs
48 | // to enable Entra Authentication. This will handle token acquisition, username extraction, and
49 | // token refresh as needed. If you copy NpgsqlDataSourceBuilderExtensions.cs into your project and
50 | // add the proper using statement, you should be able to directly call this method on a NpgsqlDataSourceBuilder
51 | // to enable Entra authentication in your application.
52 | if (useAsync)
53 | {
54 | await dataSourceBuilder.UseEntraAuthenticationAsync();
55 | }
56 | else
57 | {
58 | dataSourceBuilder.UseEntraAuthentication();
59 | }
60 |
61 | using var dataSource = dataSourceBuilder.Build();
62 | await using var connection = await dataSource.OpenConnectionAsync();
63 |
64 | // Get PostgreSQL version
65 | using var cmd1 = new NpgsqlCommand("SELECT version()", connection);
66 | var version = await cmd1.ExecuteScalarAsync();
67 | Console.WriteLine($"PostgreSQL Version: {version}");
68 | }
69 |
70 | private static string BuildConnectionString()
71 | {
72 | // Read configuration values from appsettings.json or environment variables
73 | var server = _configuration["Host"];
74 | var database = _configuration["Database"] ?? "postgres";
75 | var port = _configuration.GetValue("Port", 5432);
76 | var sslMode = _configuration["SslMode"] ?? "Require";
77 | if (string.IsNullOrEmpty(server))
78 | {
79 | throw new InvalidOperationException("Host must be configured in appsettings.json or as environment variable 'Host'");
80 | }
81 |
82 | return $"Host={server};Database={database};Port={port};SSL Mode={sslMode};";
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/python/psycopg3/sample.py:
--------------------------------------------------------------------------------
1 | """
2 | Sample demonstrating both synchronous and asynchronous psycopg3 connections
3 | with Azure Entra ID authentication for Azure PostgreSQL.
4 | """
5 |
6 | import argparse
7 | import asyncio
8 | import os
9 | import sys
10 |
11 | from dotenv import load_dotenv
12 | from psycopg_pool import AsyncConnectionPool, ConnectionPool
13 | from entra_connection import EntraConnection
14 | from async_entra_connection import AsyncEntraConnection
15 |
16 | # Load environment variables from .env file
17 | load_dotenv()
18 | hostname = os.getenv("HOSTNAME")
19 | database = os.getenv("DATABASE", "postgres")
20 |
21 |
22 | def main_sync() -> None:
23 | """Synchronous connection example using psycopg with Entra ID authentication."""
24 |
25 | # We use the EntraConnection class to enable synchronous Entra-based authentication for database access.
26 | # This class is applied whenever the connection pool creates a new connection, ensuring that Entra
27 | # authentication tokens are properly managed and refreshed so that each connection uses a valid token.
28 | #
29 | # For more details, see: https://www.psycopg.org/psycopg3/docs/api/connections.html#psycopg.Connection.connect
30 | pool = ConnectionPool(
31 | conninfo=f"postgresql://{hostname}:5432/{database}",
32 | min_size=1,
33 | max_size=5,
34 | open=False,
35 | connection_class=EntraConnection,
36 | )
37 | with pool, pool.connection() as conn, conn.cursor() as cur:
38 | cur.execute("SELECT now()")
39 | result = cur.fetchone()
40 | print(f"Sync - Database time: {result}")
41 |
42 | async def main_async() -> None:
43 | """Asynchronous connection example using psycopg with Entra ID authentication."""
44 |
45 | # We use the AsyncEntraConnection class to enable asynchronous Entra-based authentication for database access.
46 | # This class is applied whenever the connection pool creates a new connection, ensuring that Entra
47 | # authentication tokens are properly managed and refreshed so that each connection uses a valid token.
48 | #
49 | # For more details, see: https://www.psycopg.org/psycopg3/docs/api/connections.html#psycopg.Connection.connect
50 | pool = AsyncConnectionPool(
51 | conninfo=f"postgresql://{hostname}:5432/{database}",
52 | min_size=1,
53 | max_size=5,
54 | open=False,
55 | connection_class=AsyncEntraConnection,
56 | )
57 | async with pool, pool.connection() as conn, conn.cursor() as cur:
58 | await cur.execute("SELECT now()")
59 | result = await cur.fetchone()
60 | print(f"Async - Database time: {result}")
61 |
62 |
63 | async def main(mode: str = "async") -> None:
64 | """Main function that runs sync and/or async examples based on mode.
65 |
66 | Args:
67 | mode: "sync", "async", or "both" to determine which examples to run
68 | """
69 | if mode in ("sync", "both"):
70 | print("=== Running Synchronous Example ===")
71 | try:
72 | main_sync()
73 | print("Sync example completed successfully!")
74 | except Exception as e:
75 | print(f"Sync example failed: {e}")
76 |
77 | if mode in ("async", "both"):
78 | if mode == "both":
79 | print("\n=== Running Asynchronous Example ===")
80 | else:
81 | print("=== Running Asynchronous Example ===")
82 | try:
83 | await main_async()
84 | print("Async example completed successfully!")
85 | except Exception as e:
86 | print(f"Async example failed: {e}")
87 |
88 |
89 | if __name__ == "__main__":
90 | # Parse command line arguments
91 | parser = argparse.ArgumentParser(
92 | description="Demonstrate psycopg connections with Azure Entra ID authentication"
93 | )
94 | parser.add_argument(
95 | "--mode",
96 | choices=["sync", "async", "both"],
97 | default="both",
98 | help="Run synchronous, asynchronous, or both examples (default: both)",
99 | )
100 | args = parser.parse_args()
101 |
102 | # Set Windows event loop policy for compatibility if needed
103 | if sys.platform.startswith("win"):
104 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
105 |
106 | asyncio.run(main(args.mode))
--------------------------------------------------------------------------------
/python/sqlalchemy/sample.py:
--------------------------------------------------------------------------------
1 | """
2 | Sample demonstrating both synchronous and asynchronous SQLAlchemy connections
3 | with Azure Entra ID authentication for Azure PostgreSQL.
4 | """
5 |
6 | import argparse
7 | import asyncio
8 | import os
9 | import sys
10 |
11 | from dotenv import load_dotenv
12 | from sqlalchemy import create_engine, text
13 | from sqlalchemy.ext.asyncio import create_async_engine
14 |
15 | from async_entra_connection import enable_entra_authentication_async
16 | from entra_connection import enable_entra_authentication
17 |
18 | # Load environment variables from .env file
19 | load_dotenv()
20 | hostname = os.getenv("HOSTNAME")
21 | database = os.getenv("DATABASE", "postgres")
22 |
23 |
24 | def main_sync() -> None:
25 | """Synchronous connection example using SQLAlchemy with Entra ID authentication."""
26 |
27 | # Create a synchronous engine
28 | engine = create_engine(f"postgresql+psycopg://{hostname}:5432/{database}")
29 |
30 | # We add an event listener to the engine to enable synchronous Entra authentication
31 | # for database access. This event listener is triggered whenever the connection pool
32 | # backing the engine creates a new connection, ensuring that Entra authentication tokens
33 | # are properly managed and refreshed so that each connection uses a valid token.
34 | #
35 | # For more details, see: https://docs.sqlalchemy.org/en/20/core/engines.html#controlling-how-parameters-are-passed-to-the-dbapi-connect-function
36 | enable_entra_authentication(engine)
37 |
38 | with engine.connect() as conn:
39 | result = conn.execute(text("SELECT now()"))
40 | row = result.fetchone()
41 | print(f"Sync - Database time: {row[0] if row else 'Unknown'}")
42 |
43 | # Clean up the engine
44 | engine.dispose()
45 |
46 |
47 | async def main_async() -> None:
48 | """Asynchronous connection example using SQLAlchemy with Entra ID authentication."""
49 |
50 | # Create an asynchronous engine
51 | engine = create_async_engine(f"postgresql+psycopg://{hostname}:5432/{database}")
52 |
53 | # We add an event listener to the engine to enable asynchronous Entra authentication
54 | # for database access. This event listener is triggered whenever the connection pool
55 | # backing the engine creates a new connection, ensuring that Entra authentication tokens
56 | # are properly managed and refreshed so that each connection uses a valid token.
57 | #
58 | # For more details, see: https://docs.sqlalchemy.org/en/20/core/engines.html#controlling-how-parameters-are-passed-to-the-dbapi-connect-function
59 | enable_entra_authentication_async(engine)
60 |
61 | async with engine.connect() as conn:
62 | result = await conn.execute(text("SELECT now()"))
63 | row = result.fetchone()
64 | print(f"Async Core - Database time: {row[0] if row else 'Unknown'}")
65 |
66 | # Clean up the engine
67 | await engine.dispose()
68 |
69 |
70 | async def main(mode: str = "async") -> None:
71 | """Main function that runs sync and/or async examples based on mode.
72 |
73 | Args:
74 | mode: "sync", "async", or "both" to determine which examples to run
75 | """
76 | if mode in ("sync", "both"):
77 | print("=== Running Synchronous SQLAlchemy Example ===")
78 | try:
79 | main_sync()
80 | print("Sync example completed successfully!")
81 | except Exception as e:
82 | print(f"Sync example failed: {e}")
83 |
84 | if mode in ("async", "both"):
85 | if mode == "both":
86 | print("\n=== Running Asynchronous SQLAlchemy Example ===")
87 | else:
88 | print("=== Running Asynchronous SQLAlchemy Example ===")
89 | try:
90 | await main_async()
91 | print("Async example completed successfully!")
92 | except Exception as e:
93 | print(f"Async example failed: {e}")
94 |
95 |
96 | if __name__ == "__main__":
97 | # Parse command line arguments
98 | parser = argparse.ArgumentParser(
99 | description="Demonstrate SQLAlchemy connections with Azure Entra ID authentication"
100 | )
101 | parser.add_argument(
102 | "--mode",
103 | choices=["sync", "async", "both"],
104 | default="both",
105 | help="Run synchronous, asynchronous, or both examples (default: both)",
106 | )
107 | args = parser.parse_args()
108 |
109 | # Set Windows event loop policy for compatibility if needed
110 | if sys.platform.startswith("win"):
111 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
112 |
113 | asyncio.run(main(args.mode))
114 |
--------------------------------------------------------------------------------
/java/EntraIdExtensionJdbc.java:
--------------------------------------------------------------------------------
1 | import java.sql.DriverManager;
2 | import java.util.Properties;
3 | import java.sql.Connection;
4 | import java.sql.ResultSet;
5 | import java.sql.Statement;
6 | import java.io.IOException;
7 | import java.io.InputStream;
8 | import java.io.FileInputStream;
9 | import com.zaxxer.hikari.HikariConfig;
10 | import com.zaxxer.hikari.HikariDataSource;
11 |
12 | public class EntraIdExtensionJdbc {
13 | public static void main(String[] args) {
14 | Properties config = loadApplicationProperties();
15 |
16 | // Get URL and user from properties
17 | String url = config.getProperty("url");
18 | String user = config.getProperty("user");
19 |
20 | if (url == null || url.trim().isEmpty()) {
21 | System.err.println("URL not found in application.properties");
22 | return;
23 | }
24 |
25 | if (user == null || user.trim().isEmpty()) {
26 | System.err.println("User not found in application.properties");
27 | return;
28 | }
29 |
30 | // Demonstrate basic JDBC connection
31 | demonstrateBasicJdbc(url, user);
32 |
33 | System.out.println("\n" + "=".repeat(60) + "\n");
34 |
35 | // Demonstrate connection pooling
36 | demonstrateConnectionPooling(url, user);
37 | }
38 |
39 | /**
40 | * Demonstrate basic JDBC connection using DriverManager and Azure
41 | * authentication plugin
42 | */
43 | private static void demonstrateBasicJdbc(String url, String user) {
44 | System.out.println("Basic JDBC Connection (no pooling):");
45 |
46 | // Create connection properties
47 | Properties props = new Properties();
48 | props.setProperty("user", user);
49 |
50 | try (Connection conn = DriverManager.getConnection(url, props)) {
51 | System.out.println("Connected successfully using automatic token retrieval!");
52 | var rs = conn.createStatement().executeQuery("SELECT current_user;");
53 | if (rs.next()) {
54 | System.out.println("Current database user: " + rs.getString(1));
55 | }
56 | } catch (Exception e) {
57 | e.printStackTrace();
58 | }
59 | }
60 |
61 | /**
62 | * Demonstrate connection pooling with HikariCP using Azure authentication
63 | * plugin
64 | */
65 | private static void demonstrateConnectionPooling(String jdbcUrl, String user) {
66 | System.out.println("Connection Pooling with HikariCP:");
67 |
68 | // Configure HikariCP with JDBC URL (the Azure plugin handles authentication)
69 | HikariConfig config = new HikariConfig();
70 | config.setJdbcUrl(jdbcUrl);
71 | config.setUsername(user);
72 |
73 | // Pool configuration
74 | config.setMaximumPoolSize(10);
75 | config.setMinimumIdle(2);
76 | config.setConnectionTimeout(30000); // 30 seconds
77 | config.setIdleTimeout(600000); // 10 minutes
78 | config.setMaxLifetime(1800000); // 30 minutes (less than token lifetime)
79 | config.setPoolName("PostgreSQL-Azure-Pool");
80 |
81 | try (HikariDataSource pooledDataSource = new HikariDataSource(config)) {
82 | System.out.println("Connection pool created with " + config.getMaximumPoolSize() + " max connections");
83 | // Execute multiple queries using the pool
84 | for (int i = 1; i <= 3; i++) {
85 | try (Connection conn = pooledDataSource.getConnection();
86 | Statement stmt = conn.createStatement();
87 | ResultSet rs = stmt.executeQuery(
88 | "SELECT 'Query #" + i + "' AS query_num, NOW() AS time, current_user AS user")) {
89 |
90 | if (rs.next()) {
91 | System.out.println(" " + rs.getString("query_num") + " - " + rs.getTimestamp("time")
92 | + " - User: " + rs.getString("user"));
93 | }
94 |
95 | } catch (Exception e) {
96 | System.err.println("Query " + i + " failed:");
97 | e.printStackTrace();
98 | }
99 | }
100 |
101 | System.out.println("All pooled queries completed successfully");
102 | System.out.println("Pool stats - Active: " + pooledDataSource.getHikariPoolMXBean().getActiveConnections()
103 | + ", Idle: " + pooledDataSource.getHikariPoolMXBean().getIdleConnections() + ", Total: "
104 | + pooledDataSource.getHikariPoolMXBean().getTotalConnections());
105 |
106 | } catch (Exception e) {
107 | System.err.println("Connection pooling failed:");
108 | e.printStackTrace();
109 | }
110 | }
111 |
112 | private static Properties loadApplicationProperties() {
113 | Properties config = new Properties();
114 | try (InputStream input = EntraIdExtensionJdbc.class.getClassLoader()
115 | .getResourceAsStream("application.properties")) {
116 | if (input == null) {
117 | System.err.println("Unable to find application.properties");
118 | System.exit(1);
119 | }
120 | config.load(input);
121 | return config;
122 | } catch (IOException e) {
123 | System.err.println("Error loading application.properties: " + e.getMessage());
124 | e.printStackTrace();
125 | System.exit(1);
126 | return config;
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/java/EntraIdExtensionHibernate.java:
--------------------------------------------------------------------------------
1 | import org.hibernate.Session;
2 | import org.hibernate.SessionFactory;
3 | import org.hibernate.cfg.Configuration;
4 | import org.hibernate.cfg.Environment;
5 | import java.util.Properties;
6 | import java.io.FileInputStream;
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 |
10 | public class EntraIdExtensionHibernate {
11 |
12 | private static SessionFactory sessionFactory;
13 |
14 | public static void main(String[] args) {
15 | try {
16 | // Create SessionFactory
17 | sessionFactory = createSessionFactory();
18 |
19 | // Test the connection
20 | testDatabaseConnection();
21 |
22 | } catch (Exception e) {
23 | System.err.println("Failed to create SessionFactory or connect to database:");
24 | e.printStackTrace();
25 | } finally {
26 | // Clean up
27 | if (sessionFactory != null) {
28 | sessionFactory.close();
29 | }
30 | }
31 | }
32 |
33 | /**
34 | * Create Hibernate SessionFactory with Azure AD authentication
35 | */
36 | private static SessionFactory createSessionFactory() {
37 | // Load configuration from application.properties
38 | Properties appProps = loadApplicationProperties();
39 |
40 | String url = appProps.getProperty("url");
41 | String user = appProps.getProperty("user");
42 |
43 | if (url == null || url.trim().isEmpty()) {
44 | throw new RuntimeException("URL not found in application.properties");
45 | }
46 |
47 | if (user == null || user.trim().isEmpty()) {
48 | throw new RuntimeException("User not found in application.properties");
49 | }
50 |
51 | // Configure Hibernate properties
52 | Properties hibernateProps = new Properties();
53 |
54 | // Database connection settings
55 | hibernateProps.setProperty("hibernate.connection.driver_class", "org.postgresql.Driver");
56 | hibernateProps.setProperty("hibernate.connection.url", url);
57 | hibernateProps.setProperty("hibernate.connection.username", user);
58 |
59 | // Hibernate settings
60 | hibernateProps.setProperty(Environment.DIALECT, "org.hibernate.dialect.PostgreSQLDialect");
61 | hibernateProps.setProperty(Environment.SHOW_SQL, "true");
62 | hibernateProps.setProperty(Environment.FORMAT_SQL, "true");
63 | hibernateProps.setProperty(Environment.HBM2DDL_AUTO, "none"); // Don't auto-create tables
64 |
65 | // Connection pool settings (using Hibernate's built-in pool)
66 | hibernateProps.setProperty(Environment.POOL_SIZE, "5");
67 | hibernateProps.setProperty(Environment.AUTOCOMMIT, "true");
68 |
69 | try {
70 | Configuration configuration = new Configuration();
71 | configuration.setProperties(hibernateProps);
72 |
73 | System.out.println("Creating Hibernate SessionFactory...");
74 | return configuration.buildSessionFactory();
75 |
76 | } catch (Exception e) {
77 | throw new RuntimeException("Failed to create SessionFactory", e);
78 | }
79 | }
80 |
81 | /**
82 | * Load properties from application.properties file
83 | */
84 | private static Properties loadApplicationProperties() {
85 | Properties props = new Properties();
86 | try (InputStream input = new FileInputStream("application.properties")) {
87 | if (input == null) {
88 | throw new RuntimeException("Unable to find application.properties");
89 | }
90 | props.load(input);
91 | return props;
92 | } catch (IOException e) {
93 | throw new RuntimeException("Error loading application.properties: " + e.getMessage(), e);
94 | }
95 | }
96 |
97 | /**
98 | * Test the database connection by executing a simple query
99 | */
100 | private static void testDatabaseConnection() {
101 | System.out.println("Testing database connection...");
102 |
103 | try (Session session = sessionFactory.openSession()) {
104 | // Execute a simple query to test the connection
105 | String currentUser = session.createNativeQuery("SELECT current_user", String.class).getSingleResult();
106 | String currentTime = session.createNativeQuery("SELECT NOW()", String.class).getSingleResult();
107 | String version = session.createNativeQuery("SELECT version()", String.class).getSingleResult();
108 |
109 | System.out.println("Successfully connected to PostgreSQL!");
110 | System.out.println("Current user: " + currentUser);
111 | System.out.println("Current time: " + currentTime);
112 | System.out.println("PostgreSQL version: " + version.substring(0, Math.min(version.length(), 50)) + "...");
113 |
114 | // Test multiple sessions to verify connection pooling
115 | System.out.println("\nTesting connection pooling...");
116 | for (int i = 1; i <= 3; i++) {
117 | try (Session testSession = sessionFactory.openSession()) {
118 | String result = testSession.createNativeQuery("SELECT 'Test query #" + i + "'", String.class)
119 | .getSingleResult();
120 | System.out.println(" " + result + " - Session created successfully");
121 | }
122 | }
123 | System.out.println("Connection pooling is working!");
124 |
125 | } catch (Exception e) {
126 | System.err.println("Database connection test failed:");
127 | e.printStackTrace();
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/dotnet/NpgsqlDataSourceBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft. All rights reserved.
2 |
3 | using System.Text;
4 | using System.Text.Json;
5 | using Azure.Core;
6 | using Azure.Identity;
7 | using Npgsql;
8 |
9 | namespace Postgres.EntraAuth;
10 |
11 | ///
12 | /// Extension methods for NpgsqlDataSourceBuilder to enable Entra authentication with Azure DB for PostgreSQL.
13 | /// This class provides methods to configure NpgsqlDataSourceBuilder to use Entra authentication, handling token
14 | /// acquisition and connection setup. It is not specific to this repository and can be used in any project that
15 | /// requires Entra authentication with Azure DB for PostgreSQL.
16 | ///
17 | /// Example usage:
18 | ///
19 | /// using Npgsql;
20 | /// using Postgres.EntraAuth;
21 | ///
22 | /// var dataSourceBuilder = new NpgsqlDataSourceBuilder("");
23 | /// dataSourceBuilder.UseEntraAuthentication();
24 | /// var dataSource = dataSourceBuilder.Build();
25 | ///
26 | ///
27 | public static class NpgsqlDataSourceBuilderExtensions
28 | {
29 | private static readonly TokenRequestContext s_azureDBForPostgresTokenRequestContext = new([Constants.AzureDBForPostgresScope]);
30 | private static readonly TokenRequestContext s_managementTokenRequestContext = new([Constants.AzureManagementScope]);
31 |
32 | ///
33 | /// Configures the NpgsqlDataSourceBuilder to use Entra ID authentication synchronously.
34 | ///
35 | /// The NpgsqlDataSourceBuilder to configure.
36 | /// The TokenCredential to use for authentication. If null, DefaultAzureCredential is used.
37 | /// A cancellation token that can be used to cancel the operation.
38 | /// The configured NpgsqlDataSourceBuilder.
39 | public static NpgsqlDataSourceBuilder UseEntraAuthentication(this NpgsqlDataSourceBuilder dataSourceBuilder, TokenCredential? credential = default, CancellationToken cancellationToken = default)
40 | {
41 | credential ??= new DefaultAzureCredential();
42 |
43 | if (dataSourceBuilder.ConnectionStringBuilder.Username == null)
44 | {
45 |
46 | // Ensure to use the management scope, so the token contains user names for all managed identity types - e.g. user and service principal
47 | var token = credential.GetToken(s_managementTokenRequestContext, cancellationToken);
48 | var username = TryGetUsernameFromToken(token.Token);
49 |
50 | if (username != null)
51 | {
52 | dataSourceBuilder.ConnectionStringBuilder.Username = username;
53 | }
54 | else
55 | {
56 | // Otherwise check using the PostgresSql scope
57 | token = credential.GetToken(s_azureDBForPostgresTokenRequestContext, cancellationToken);
58 | SetUsernameFromToken(dataSourceBuilder, token.Token);
59 | }
60 | }
61 |
62 | SetPasswordProvider(dataSourceBuilder, credential, s_azureDBForPostgresTokenRequestContext);
63 |
64 | return dataSourceBuilder;
65 | }
66 |
67 | ///
68 | /// Configures the NpgsqlDataSourceBuilder to use Entra ID authentication asynchronously.
69 | ///
70 | /// The NpgsqlDataSourceBuilder to configure.
71 | /// The TokenCredential to use for authentication. If null, DefaultAzureCredential is used.
72 | /// A cancellation token that can be used to cancel the operation.
73 | /// A task that represents the asynchronous operation. The task result contains the configured NpgsqlDataSourceBuilder.
74 | public static async Task UseEntraAuthenticationAsync(this NpgsqlDataSourceBuilder dataSourceBuilder, TokenCredential? credential = default, CancellationToken cancellationToken = default)
75 | {
76 | credential ??= new DefaultAzureCredential();
77 |
78 | if (dataSourceBuilder.ConnectionStringBuilder.Username == null)
79 | {
80 |
81 | // Ensure to use the management scope, so the token contains user names for all managed identity types - e.g. user and service principal
82 | var token = await credential.GetTokenAsync(s_managementTokenRequestContext, cancellationToken).ConfigureAwait(false);
83 | var username = TryGetUsernameFromToken(token.Token);
84 |
85 | if (username != null)
86 | {
87 | dataSourceBuilder.ConnectionStringBuilder.Username = username;
88 | }
89 | else
90 | {
91 | // Otherwise check using the PostgresSql scope
92 | token = await credential.GetTokenAsync(s_azureDBForPostgresTokenRequestContext, cancellationToken).ConfigureAwait(false);
93 | SetUsernameFromToken(dataSourceBuilder, token.Token);
94 | }
95 | }
96 |
97 | SetPasswordProvider(dataSourceBuilder, credential, s_azureDBForPostgresTokenRequestContext);
98 |
99 | return dataSourceBuilder;
100 | }
101 |
102 | private static void SetPasswordProvider(NpgsqlDataSourceBuilder dataSourceBuilder, TokenCredential credential, TokenRequestContext tokenRequestContext)
103 | {
104 | dataSourceBuilder.UsePasswordProvider(_ =>
105 | {
106 | var token = credential.GetToken(tokenRequestContext, default);
107 | return token.Token;
108 | }, async (_, ct) =>
109 | {
110 | // Use the cancellation token provided by Npgsql for async operations
111 | var token = await credential.GetTokenAsync(tokenRequestContext, ct).ConfigureAwait(false);
112 | return token.Token;
113 | });
114 | }
115 |
116 | private static void SetUsernameFromToken(NpgsqlDataSourceBuilder dataSourceBuilder, string token)
117 | {
118 | var username = TryGetUsernameFromToken(token);
119 |
120 | if (username != null)
121 | {
122 | dataSourceBuilder.ConnectionStringBuilder.Username = username;
123 | }
124 | else
125 | {
126 | throw new Exception("Could not determine username from token claims");
127 | }
128 | }
129 |
130 | private static string? TryGetUsernameFromToken(string jwtToken)
131 | {
132 | // Split the token into its parts (Header, Payload, Signature)
133 | var tokenParts = jwtToken.Split('.');
134 | if (tokenParts.Length != 3)
135 | {
136 | return null;
137 | }
138 |
139 | // The payload is the second part, Base64Url encoded
140 | var payload = tokenParts[1];
141 | if (string.IsNullOrWhiteSpace(payload))
142 | {
143 | return null; // empty payload
144 | }
145 |
146 | try
147 | {
148 | // Add padding if necessary
149 | payload = AddBase64Padding(payload);
150 |
151 | // Convert from Base64Url to standard Base64
152 | payload = payload.Replace('-', '+').Replace('_', '/');
153 |
154 | // Decode the payload from Base64
155 | var decodedBytes = Convert.FromBase64String(payload);
156 | var decodedPayload = Encoding.UTF8.GetString(decodedBytes);
157 |
158 | if (string.IsNullOrWhiteSpace(decodedPayload))
159 | {
160 | return null; // nothing to parse
161 | }
162 |
163 | // Parse the decoded payload as JSON
164 | var payloadJson = JsonSerializer.Deserialize(decodedPayload);
165 |
166 | // Try to get the username from 'xms_mirid', 'upn', 'preferred_username', or 'unique_name' claims
167 | if (payloadJson.TryGetProperty("xms_mirid", out var xms_mirid) &&
168 | xms_mirid.GetString() is string xms_miridString &&
169 | ParsePrincipalName(xms_miridString) is string principalName)
170 | {
171 | return principalName;
172 | }
173 | else if (payloadJson.TryGetProperty("upn", out var upn))
174 | {
175 | return upn.GetString();
176 | }
177 | else if (payloadJson.TryGetProperty("preferred_username", out var preferredUsername))
178 | {
179 | return preferredUsername.GetString();
180 | }
181 | else if (payloadJson.TryGetProperty("unique_name", out var uniqueName))
182 | {
183 | return uniqueName.GetString();
184 | }
185 |
186 | return null; // no relevant claims
187 | }
188 | catch (FormatException)
189 | {
190 | // Invalid Base64 content
191 | return null;
192 | }
193 | catch (JsonException)
194 | {
195 | // Invalid JSON content
196 | return null;
197 | }
198 | }
199 |
200 | private static string? ParsePrincipalName(string xms_mirid)
201 | {
202 | // parse the xms_mirid claim which looks like
203 | // /subscriptions/{subId}/resourcegroups/{resourceGroup}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{principalName}
204 | var lastSlashIndex = xms_mirid.LastIndexOf('/');
205 | if (lastSlashIndex == -1)
206 | {
207 | return null;
208 | }
209 |
210 | var beginning = xms_mirid.AsSpan(0, lastSlashIndex);
211 | var principalName = xms_mirid.AsSpan(lastSlashIndex + 1);
212 |
213 | if (principalName.IsEmpty || !beginning.EndsWith("providers/Microsoft.ManagedIdentity/userAssignedIdentities", StringComparison.OrdinalIgnoreCase))
214 | {
215 | return null;
216 | }
217 |
218 | return principalName.ToString();
219 | }
220 |
221 | private static string AddBase64Padding(string base64) => (base64.Length % 4) switch
222 | {
223 | 2 => base64 + "==",
224 | 3 => base64 + "=",
225 | _ => base64,
226 | };
227 | }
228 |
--------------------------------------------------------------------------------
/python/psycopg2/shared.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | import base64
4 | import json
5 | from typing import Any, cast
6 |
7 | from azure.core.credentials import TokenCredential
8 | from azure.core.credentials_async import AsyncTokenCredential
9 | from azure.core.exceptions import ClientAuthenticationError
10 | from azure.identity import CredentialUnavailableError
11 | from azure.identity import DefaultAzureCredential as DefaultAzureCredential
12 | from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential
13 |
14 | from errors import (
15 | ScopePermissionError,
16 | TokenDecodeError,
17 | UsernameExtractionError,
18 | )
19 |
20 | AZURE_DB_FOR_POSTGRES_SCOPE = "https://ossrdbms-aad.database.windows.net/.default"
21 | AZURE_MANAGEMENT_SCOPE = "https://management.azure.com/.default"
22 |
23 |
24 | def get_entra_token(credential: TokenCredential | None, scope: str) -> str:
25 | """Acquires an Entra authentication token for Azure PostgreSQL synchronously.
26 |
27 | Parameters:
28 | credential (TokenCredential or None): Credential object used to obtain the token.
29 | If None, the default Azure credentials are used.
30 | scope (str): The scope for the token request.
31 |
32 | Returns:
33 | str: The acquired authentication token to be used as the database password.
34 | """
35 | credential = credential or DefaultAzureCredential()
36 | cred = credential.get_token(scope)
37 | return cred.token
38 |
39 |
40 | async def get_entra_token_async(
41 | credential: AsyncTokenCredential | None, scope: str
42 | ) -> str:
43 | """Asynchronously acquires an Entra authentication token for Azure PostgreSQL.
44 |
45 | Parameters:
46 | credential (AsyncTokenCredential or None): Asynchronous credential used to obtain the token.
47 | If None, the default Azure credentials are used.
48 | scope (str): The scope for the token request.
49 |
50 | Returns:
51 | str: The acquired authentication token to be used as the database password.
52 | """
53 | credential = credential or AsyncDefaultAzureCredential()
54 | async with credential:
55 | cred = await credential.get_token(scope)
56 | return cred.token
57 |
58 |
59 | def decode_jwt(token: str) -> dict[str, Any]:
60 | """Decodes a JWT token to extract its payload claims.
61 |
62 | Parameters:
63 | token (str): The JWT token string in the standard three-part format.
64 |
65 | Returns:
66 | dict[str, Any]: A dictionary containing the claims extracted from the token payload.
67 |
68 | Raises:
69 | TokenValueError: If the token format is invalid or cannot be decoded.
70 | """
71 | try:
72 | payload = token.split(".")[1]
73 | padding = "=" * (4 - len(payload) % 4)
74 | decoded_payload = base64.urlsafe_b64decode(payload + padding)
75 | return cast(dict[str, Any], json.loads(decoded_payload))
76 | except Exception as e:
77 | raise TokenDecodeError("Invalid JWT token format") from e
78 |
79 |
80 | def parse_principal_name(xms_mirid: str) -> str | None:
81 | """Parses the principal name from an Azure resource path.
82 |
83 | Parameters:
84 | xms_mirid (str): The xms_mirid claim value containing the Azure resource path.
85 |
86 | Returns:
87 | str | None: The extracted principal name, or None if parsing fails.
88 | """
89 | if not xms_mirid:
90 | return None
91 |
92 | # Parse the xms_mirid claim which looks like
93 | # /subscriptions/{subId}/resourcegroups/{resourceGroup}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{principalName}
94 | last_slash_index = xms_mirid.rfind("/")
95 | if last_slash_index == -1:
96 | return None
97 |
98 | beginning = xms_mirid[:last_slash_index]
99 | principal_name = xms_mirid[last_slash_index + 1 :]
100 |
101 | if not principal_name or not beginning.lower().endswith(
102 | "providers/microsoft.managedidentity/userassignedidentities"
103 | ):
104 | return None
105 |
106 | return principal_name
107 |
108 |
109 | def get_entra_conninfo(credential: TokenCredential | None) -> dict[str, str]:
110 | """Synchronously obtains connection information from Entra authentication for Azure PostgreSQL.
111 |
112 | This function acquires an access token from Azure Entra ID and extracts the username
113 | from the token claims. It tries multiple claim sources to determine the username.
114 |
115 | Parameters:
116 | credential (TokenCredential or None): The credential used for token acquisition.
117 | If None, DefaultAzureCredential() is used to automatically discover credentials.
118 |
119 | Returns:
120 | dict[str, str]: A dictionary with 'user' and 'password' keys, where:
121 | - 'user': The extracted username from token claims
122 | - 'password': The Entra ID access token for database authentication
123 |
124 | Raises:
125 | TokenDecodeError: If the JWT token cannot be decoded or is malformed.
126 | UsernameExtractionError: If the username cannot be extracted from token claims.
127 | ScopePermissionError: The token could not be acquired from the management scope, possibly due to insufficient permissions.
128 | """
129 | credential = credential or DefaultAzureCredential()
130 |
131 | # Always get the DB-scope token for password
132 | db_token = get_entra_token(credential, AZURE_DB_FOR_POSTGRES_SCOPE)
133 | try:
134 | db_claims = decode_jwt(db_token)
135 | except TokenDecodeError:
136 | raise
137 | xms_mirid = db_claims.get("xms_mirid")
138 | username = (
139 | parse_principal_name(xms_mirid)
140 | if isinstance(xms_mirid, str)
141 | else None
142 | or db_claims.get("upn")
143 | or db_claims.get("preferred_username")
144 | or db_claims.get("unique_name")
145 | )
146 |
147 | if not username:
148 | # Fall back to management scope ONLY to discover username
149 | try:
150 | mgmt_token = get_entra_token(credential, AZURE_MANAGEMENT_SCOPE)
151 | except (CredentialUnavailableError, ClientAuthenticationError) as e:
152 | raise ScopePermissionError(
153 | "Failed to acquire token from management scope"
154 | ) from e
155 | try:
156 | mgmt_claims = decode_jwt(mgmt_token)
157 | except TokenDecodeError:
158 | raise
159 | xms_mirid = mgmt_claims.get("xms_mirid")
160 | username = (
161 | parse_principal_name(xms_mirid)
162 | if isinstance(xms_mirid, str)
163 | else None
164 | or mgmt_claims.get("upn")
165 | or mgmt_claims.get("preferred_username")
166 | or mgmt_claims.get("unique_name")
167 | )
168 |
169 | if not username:
170 | raise UsernameExtractionError(
171 | "Could not determine username from token claims. "
172 | "Ensure the identity has the proper Azure AD attributes."
173 | )
174 |
175 | return {"user": username, "password": db_token}
176 |
177 |
178 | async def get_entra_conninfo_async(
179 | credential: AsyncTokenCredential | None,
180 | ) -> dict[str, str]:
181 | """Asynchronously obtains connection information from Entra authentication for Azure PostgreSQL.
182 |
183 | This function acquires an access token from Azure Entra ID and extracts the username
184 | from the token claims. It tries multiple claim sources to determine the username.
185 |
186 | Parameters:
187 | credential (AsyncTokenCredential or None): The async credential used for token acquisition.
188 | If None, AsyncDefaultAzureCredential() is used to automatically discover credentials.
189 |
190 | Returns:
191 | dict[str, str]: A dictionary with 'user' and 'password' keys, where:
192 | - 'user': The extracted username from token claims
193 | - 'password': The Entra ID access token for database authentication
194 |
195 | Raises:
196 | TokenDecodeError: If the JWT token cannot be decoded or is malformed.
197 | UsernameExtractionError: If the username cannot be extracted from token claims.
198 | ScopePermissionError: The token could not be acquired from the management scope, possibly due to insufficient permissions.
199 | """
200 | credential = credential or AsyncDefaultAzureCredential()
201 |
202 | db_token = await get_entra_token_async(credential, AZURE_DB_FOR_POSTGRES_SCOPE)
203 | try:
204 | db_claims = decode_jwt(db_token)
205 | except TokenDecodeError:
206 | raise
207 | xms_mirid = db_claims.get("xms_mirid")
208 | username = (
209 | parse_principal_name(xms_mirid)
210 | if isinstance(xms_mirid, str)
211 | else None
212 | or db_claims.get("upn")
213 | or db_claims.get("preferred_username")
214 | or db_claims.get("unique_name")
215 | )
216 |
217 | if not username:
218 | try:
219 | mgmt_token = await get_entra_token_async(credential, AZURE_MANAGEMENT_SCOPE)
220 | except (CredentialUnavailableError, ClientAuthenticationError) as e:
221 | raise ScopePermissionError(
222 | "Failed to acquire token from management scope"
223 | ) from e
224 | try:
225 | mgmt_claims = decode_jwt(mgmt_token)
226 | except TokenDecodeError:
227 | raise
228 | xms_mirid = mgmt_claims.get("xms_mirid")
229 | username = (
230 | parse_principal_name(xms_mirid)
231 | if isinstance(xms_mirid, str)
232 | else None
233 | or mgmt_claims.get("upn")
234 | or mgmt_claims.get("preferred_username")
235 | or mgmt_claims.get("unique_name")
236 | )
237 |
238 | if not username:
239 | raise UsernameExtractionError(
240 | "Could not determine username from token claims. "
241 | "Ensure the identity has the proper Azure AD attributes."
242 | )
243 |
244 | return {"user": username, "password": db_token}
245 |
--------------------------------------------------------------------------------
/python/psycopg3/shared.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | import base64
4 | import json
5 | from typing import Any, cast
6 |
7 | from azure.core.credentials import TokenCredential
8 | from azure.core.credentials_async import AsyncTokenCredential
9 | from azure.core.exceptions import ClientAuthenticationError
10 | from azure.identity import CredentialUnavailableError
11 | from azure.identity import DefaultAzureCredential as DefaultAzureCredential
12 | from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential
13 |
14 | from errors import (
15 | ScopePermissionError,
16 | TokenDecodeError,
17 | UsernameExtractionError,
18 | )
19 |
20 | AZURE_DB_FOR_POSTGRES_SCOPE = "https://ossrdbms-aad.database.windows.net/.default"
21 | AZURE_MANAGEMENT_SCOPE = "https://management.azure.com/.default"
22 |
23 |
24 | def get_entra_token(credential: TokenCredential | None, scope: str) -> str:
25 | """Acquires an Entra authentication token for Azure PostgreSQL synchronously.
26 |
27 | Parameters:
28 | credential (TokenCredential or None): Credential object used to obtain the token.
29 | If None, the default Azure credentials are used.
30 | scope (str): The scope for the token request.
31 |
32 | Returns:
33 | str: The acquired authentication token to be used as the database password.
34 | """
35 | credential = credential or DefaultAzureCredential()
36 | cred = credential.get_token(scope)
37 | return cred.token
38 |
39 |
40 | async def get_entra_token_async(
41 | credential: AsyncTokenCredential | None, scope: str
42 | ) -> str:
43 | """Asynchronously acquires an Entra authentication token for Azure PostgreSQL.
44 |
45 | Parameters:
46 | credential (AsyncTokenCredential or None): Asynchronous credential used to obtain the token.
47 | If None, the default Azure credentials are used.
48 | scope (str): The scope for the token request.
49 |
50 | Returns:
51 | str: The acquired authentication token to be used as the database password.
52 | """
53 | credential = credential or AsyncDefaultAzureCredential()
54 | async with credential:
55 | cred = await credential.get_token(scope)
56 | return cred.token
57 |
58 |
59 | def decode_jwt(token: str) -> dict[str, Any]:
60 | """Decodes a JWT token to extract its payload claims.
61 |
62 | Parameters:
63 | token (str): The JWT token string in the standard three-part format.
64 |
65 | Returns:
66 | dict[str, Any]: A dictionary containing the claims extracted from the token payload.
67 |
68 | Raises:
69 | TokenValueError: If the token format is invalid or cannot be decoded.
70 | """
71 | try:
72 | payload = token.split(".")[1]
73 | padding = "=" * (4 - len(payload) % 4)
74 | decoded_payload = base64.urlsafe_b64decode(payload + padding)
75 | return cast(dict[str, Any], json.loads(decoded_payload))
76 | except Exception as e:
77 | raise TokenDecodeError("Invalid JWT token format") from e
78 |
79 |
80 | def parse_principal_name(xms_mirid: str) -> str | None:
81 | """Parses the principal name from an Azure resource path.
82 |
83 | Parameters:
84 | xms_mirid (str): The xms_mirid claim value containing the Azure resource path.
85 |
86 | Returns:
87 | str | None: The extracted principal name, or None if parsing fails.
88 | """
89 | if not xms_mirid:
90 | return None
91 |
92 | # Parse the xms_mirid claim which looks like
93 | # /subscriptions/{subId}/resourcegroups/{resourceGroup}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{principalName}
94 | last_slash_index = xms_mirid.rfind("/")
95 | if last_slash_index == -1:
96 | return None
97 |
98 | beginning = xms_mirid[:last_slash_index]
99 | principal_name = xms_mirid[last_slash_index + 1 :]
100 |
101 | if not principal_name or not beginning.lower().endswith(
102 | "providers/microsoft.managedidentity/userassignedidentities"
103 | ):
104 | return None
105 |
106 | return principal_name
107 |
108 |
109 | def get_entra_conninfo(credential: TokenCredential | None) -> dict[str, str]:
110 | """Synchronously obtains connection information from Entra authentication for Azure PostgreSQL.
111 |
112 | This function acquires an access token from Azure Entra ID and extracts the username
113 | from the token claims. It tries multiple claim sources to determine the username.
114 |
115 | Parameters:
116 | credential (TokenCredential or None): The credential used for token acquisition.
117 | If None, DefaultAzureCredential() is used to automatically discover credentials.
118 |
119 | Returns:
120 | dict[str, str]: A dictionary with 'user' and 'password' keys, where:
121 | - 'user': The extracted username from token claims
122 | - 'password': The Entra ID access token for database authentication
123 |
124 | Raises:
125 | TokenDecodeError: If the JWT token cannot be decoded or is malformed.
126 | UsernameExtractionError: If the username cannot be extracted from token claims.
127 | ScopePermissionError: The token could not be acquired from the management scope, possibly due to insufficient permissions.
128 | """
129 | credential = credential or DefaultAzureCredential()
130 |
131 | # Always get the DB-scope token for password
132 | db_token = get_entra_token(credential, AZURE_DB_FOR_POSTGRES_SCOPE)
133 | try:
134 | db_claims = decode_jwt(db_token)
135 | except TokenDecodeError:
136 | raise
137 | xms_mirid = db_claims.get("xms_mirid")
138 | username = (
139 | parse_principal_name(xms_mirid)
140 | if isinstance(xms_mirid, str)
141 | else None
142 | or db_claims.get("upn")
143 | or db_claims.get("preferred_username")
144 | or db_claims.get("unique_name")
145 | )
146 |
147 | if not username:
148 | # Fall back to management scope ONLY to discover username
149 | try:
150 | mgmt_token = get_entra_token(credential, AZURE_MANAGEMENT_SCOPE)
151 | except (CredentialUnavailableError, ClientAuthenticationError) as e:
152 | raise ScopePermissionError(
153 | "Failed to acquire token from management scope"
154 | ) from e
155 | try:
156 | mgmt_claims = decode_jwt(mgmt_token)
157 | except TokenDecodeError:
158 | raise
159 | xms_mirid = mgmt_claims.get("xms_mirid")
160 | username = (
161 | parse_principal_name(xms_mirid)
162 | if isinstance(xms_mirid, str)
163 | else None
164 | or mgmt_claims.get("upn")
165 | or mgmt_claims.get("preferred_username")
166 | or mgmt_claims.get("unique_name")
167 | )
168 |
169 | if not username:
170 | raise UsernameExtractionError(
171 | "Could not determine username from token claims. "
172 | "Ensure the identity has the proper Azure AD attributes."
173 | )
174 |
175 | return {"user": username, "password": db_token}
176 |
177 |
178 | async def get_entra_conninfo_async(
179 | credential: AsyncTokenCredential | None,
180 | ) -> dict[str, str]:
181 | """Asynchronously obtains connection information from Entra authentication for Azure PostgreSQL.
182 |
183 | This function acquires an access token from Azure Entra ID and extracts the username
184 | from the token claims. It tries multiple claim sources to determine the username.
185 |
186 | Parameters:
187 | credential (AsyncTokenCredential or None): The async credential used for token acquisition.
188 | If None, AsyncDefaultAzureCredential() is used to automatically discover credentials.
189 |
190 | Returns:
191 | dict[str, str]: A dictionary with 'user' and 'password' keys, where:
192 | - 'user': The extracted username from token claims
193 | - 'password': The Entra ID access token for database authentication
194 |
195 | Raises:
196 | TokenDecodeError: If the JWT token cannot be decoded or is malformed.
197 | UsernameExtractionError: If the username cannot be extracted from token claims.
198 | ScopePermissionError: The token could not be acquired from the management scope, possibly due to insufficient permissions.
199 | """
200 | credential = credential or AsyncDefaultAzureCredential()
201 |
202 | db_token = await get_entra_token_async(credential, AZURE_DB_FOR_POSTGRES_SCOPE)
203 | try:
204 | db_claims = decode_jwt(db_token)
205 | except TokenDecodeError:
206 | raise
207 | xms_mirid = db_claims.get("xms_mirid")
208 | username = (
209 | parse_principal_name(xms_mirid)
210 | if isinstance(xms_mirid, str)
211 | else None
212 | or db_claims.get("upn")
213 | or db_claims.get("preferred_username")
214 | or db_claims.get("unique_name")
215 | )
216 |
217 | if not username:
218 | try:
219 | mgmt_token = await get_entra_token_async(credential, AZURE_MANAGEMENT_SCOPE)
220 | except (CredentialUnavailableError, ClientAuthenticationError) as e:
221 | raise ScopePermissionError(
222 | "Failed to acquire token from management scope"
223 | ) from e
224 | try:
225 | mgmt_claims = decode_jwt(mgmt_token)
226 | except TokenDecodeError:
227 | raise
228 | xms_mirid = mgmt_claims.get("xms_mirid")
229 | username = (
230 | parse_principal_name(xms_mirid)
231 | if isinstance(xms_mirid, str)
232 | else None
233 | or mgmt_claims.get("upn")
234 | or mgmt_claims.get("preferred_username")
235 | or mgmt_claims.get("unique_name")
236 | )
237 |
238 | if not username:
239 | raise UsernameExtractionError(
240 | "Could not determine username from token claims. "
241 | "Ensure the identity has the proper Azure AD attributes."
242 | )
243 |
244 | return {"user": username, "password": db_token}
245 |
--------------------------------------------------------------------------------
/python/sqlalchemy/shared.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Microsoft. All rights reserved.
2 |
3 | import base64
4 | import json
5 | from typing import Any, cast
6 |
7 | from azure.core.credentials import TokenCredential
8 | from azure.core.credentials_async import AsyncTokenCredential
9 | from azure.core.exceptions import ClientAuthenticationError
10 | from azure.identity import CredentialUnavailableError
11 | from azure.identity import DefaultAzureCredential as DefaultAzureCredential
12 | from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential
13 |
14 | from errors import (
15 | ScopePermissionError,
16 | TokenDecodeError,
17 | UsernameExtractionError,
18 | )
19 |
20 | AZURE_DB_FOR_POSTGRES_SCOPE = "https://ossrdbms-aad.database.windows.net/.default"
21 | AZURE_MANAGEMENT_SCOPE = "https://management.azure.com/.default"
22 |
23 |
24 | def get_entra_token(credential: TokenCredential | None, scope: str) -> str:
25 | """Acquires an Entra authentication token for Azure PostgreSQL synchronously.
26 |
27 | Parameters:
28 | credential (TokenCredential or None): Credential object used to obtain the token.
29 | If None, the default Azure credentials are used.
30 | scope (str): The scope for the token request.
31 |
32 | Returns:
33 | str: The acquired authentication token to be used as the database password.
34 | """
35 | credential = credential or DefaultAzureCredential()
36 | cred = credential.get_token(scope)
37 | return cred.token
38 |
39 |
40 | async def get_entra_token_async(
41 | credential: AsyncTokenCredential | None, scope: str
42 | ) -> str:
43 | """Asynchronously acquires an Entra authentication token for Azure PostgreSQL.
44 |
45 | Parameters:
46 | credential (AsyncTokenCredential or None): Asynchronous credential used to obtain the token.
47 | If None, the default Azure credentials are used.
48 | scope (str): The scope for the token request.
49 |
50 | Returns:
51 | str: The acquired authentication token to be used as the database password.
52 | """
53 | credential = credential or AsyncDefaultAzureCredential()
54 | async with credential:
55 | cred = await credential.get_token(scope)
56 | return cred.token
57 |
58 |
59 | def decode_jwt(token: str) -> dict[str, Any]:
60 | """Decodes a JWT token to extract its payload claims.
61 |
62 | Parameters:
63 | token (str): The JWT token string in the standard three-part format.
64 |
65 | Returns:
66 | dict[str, Any]: A dictionary containing the claims extracted from the token payload.
67 |
68 | Raises:
69 | TokenValueError: If the token format is invalid or cannot be decoded.
70 | """
71 | try:
72 | payload = token.split(".")[1]
73 | padding = "=" * (4 - len(payload) % 4)
74 | decoded_payload = base64.urlsafe_b64decode(payload + padding)
75 | return cast(dict[str, Any], json.loads(decoded_payload))
76 | except Exception as e:
77 | raise TokenDecodeError("Invalid JWT token format") from e
78 |
79 |
80 | def parse_principal_name(xms_mirid: str) -> str | None:
81 | """Parses the principal name from an Azure resource path.
82 |
83 | Parameters:
84 | xms_mirid (str): The xms_mirid claim value containing the Azure resource path.
85 |
86 | Returns:
87 | str | None: The extracted principal name, or None if parsing fails.
88 | """
89 | if not xms_mirid:
90 | return None
91 |
92 | # Parse the xms_mirid claim which looks like
93 | # /subscriptions/{subId}/resourcegroups/{resourceGroup}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{principalName}
94 | last_slash_index = xms_mirid.rfind("/")
95 | if last_slash_index == -1:
96 | return None
97 |
98 | beginning = xms_mirid[:last_slash_index]
99 | principal_name = xms_mirid[last_slash_index + 1 :]
100 |
101 | if not principal_name or not beginning.lower().endswith(
102 | "providers/microsoft.managedidentity/userassignedidentities"
103 | ):
104 | return None
105 |
106 | return principal_name
107 |
108 |
109 | def get_entra_conninfo(credential: TokenCredential | None) -> dict[str, str]:
110 | """Synchronously obtains connection information from Entra authentication for Azure PostgreSQL.
111 |
112 | This function acquires an access token from Azure Entra ID and extracts the username
113 | from the token claims. It tries multiple claim sources to determine the username.
114 |
115 | Parameters:
116 | credential (TokenCredential or None): The credential used for token acquisition.
117 | If None, DefaultAzureCredential() is used to automatically discover credentials.
118 |
119 | Returns:
120 | dict[str, str]: A dictionary with 'user' and 'password' keys, where:
121 | - 'user': The extracted username from token claims
122 | - 'password': The Entra ID access token for database authentication
123 |
124 | Raises:
125 | TokenDecodeError: If the JWT token cannot be decoded or is malformed.
126 | UsernameExtractionError: If the username cannot be extracted from token claims.
127 | ScopePermissionError: The token could not be acquired from the management scope, possibly due to insufficient permissions.
128 | """
129 | credential = credential or DefaultAzureCredential()
130 |
131 | # Always get the DB-scope token for password
132 | db_token = get_entra_token(credential, AZURE_DB_FOR_POSTGRES_SCOPE)
133 | try:
134 | db_claims = decode_jwt(db_token)
135 | except TokenDecodeError:
136 | raise
137 | xms_mirid = db_claims.get("xms_mirid")
138 | username = (
139 | parse_principal_name(xms_mirid)
140 | if isinstance(xms_mirid, str)
141 | else None
142 | or db_claims.get("upn")
143 | or db_claims.get("preferred_username")
144 | or db_claims.get("unique_name")
145 | )
146 |
147 | if not username:
148 | # Fall back to management scope ONLY to discover username
149 | try:
150 | mgmt_token = get_entra_token(credential, AZURE_MANAGEMENT_SCOPE)
151 | except (CredentialUnavailableError, ClientAuthenticationError) as e:
152 | raise ScopePermissionError(
153 | "Failed to acquire token from management scope"
154 | ) from e
155 | try:
156 | mgmt_claims = decode_jwt(mgmt_token)
157 | except TokenDecodeError:
158 | raise
159 | xms_mirid = mgmt_claims.get("xms_mirid")
160 | username = (
161 | parse_principal_name(xms_mirid)
162 | if isinstance(xms_mirid, str)
163 | else None
164 | or mgmt_claims.get("upn")
165 | or mgmt_claims.get("preferred_username")
166 | or mgmt_claims.get("unique_name")
167 | )
168 |
169 | if not username:
170 | raise UsernameExtractionError(
171 | "Could not determine username from token claims. "
172 | "Ensure the identity has the proper Azure AD attributes."
173 | )
174 |
175 | return {"user": username, "password": db_token}
176 |
177 |
178 | async def get_entra_conninfo_async(
179 | credential: AsyncTokenCredential | None,
180 | ) -> dict[str, str]:
181 | """Asynchronously obtains connection information from Entra authentication for Azure PostgreSQL.
182 |
183 | This function acquires an access token from Azure Entra ID and extracts the username
184 | from the token claims. It tries multiple claim sources to determine the username.
185 |
186 | Parameters:
187 | credential (AsyncTokenCredential or None): The async credential used for token acquisition.
188 | If None, AsyncDefaultAzureCredential() is used to automatically discover credentials.
189 |
190 | Returns:
191 | dict[str, str]: A dictionary with 'user' and 'password' keys, where:
192 | - 'user': The extracted username from token claims
193 | - 'password': The Entra ID access token for database authentication
194 |
195 | Raises:
196 | TokenDecodeError: If the JWT token cannot be decoded or is malformed.
197 | UsernameExtractionError: If the username cannot be extracted from token claims.
198 | ScopePermissionError: The token could not be acquired from the management scope, possibly due to insufficient permissions.
199 | """
200 | credential = credential or AsyncDefaultAzureCredential()
201 |
202 | db_token = await get_entra_token_async(credential, AZURE_DB_FOR_POSTGRES_SCOPE)
203 | try:
204 | db_claims = decode_jwt(db_token)
205 | except TokenDecodeError:
206 | raise
207 | xms_mirid = db_claims.get("xms_mirid")
208 | username = (
209 | parse_principal_name(xms_mirid)
210 | if isinstance(xms_mirid, str)
211 | else None
212 | or db_claims.get("upn")
213 | or db_claims.get("preferred_username")
214 | or db_claims.get("unique_name")
215 | )
216 |
217 | if not username:
218 | try:
219 | mgmt_token = await get_entra_token_async(credential, AZURE_MANAGEMENT_SCOPE)
220 | except (CredentialUnavailableError, ClientAuthenticationError) as e:
221 | raise ScopePermissionError(
222 | "Failed to acquire token from management scope"
223 | ) from e
224 | try:
225 | mgmt_claims = decode_jwt(mgmt_token)
226 | except TokenDecodeError:
227 | raise
228 | xms_mirid = mgmt_claims.get("xms_mirid")
229 | username = (
230 | parse_principal_name(xms_mirid)
231 | if isinstance(xms_mirid, str)
232 | else None
233 | or mgmt_claims.get("upn")
234 | or mgmt_claims.get("preferred_username")
235 | or mgmt_claims.get("unique_name")
236 | )
237 |
238 | if not username:
239 | raise UsernameExtractionError(
240 | "Could not determine username from token claims. "
241 | "Ensure the identity has the proper Azure AD attributes."
242 | )
243 |
244 | return {"user": username, "password": db_token}
245 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Access Token Refresh with Entra ID for Azure Database for PostgreSQL
2 |
3 | This repository provides sample implementations in Python, JavaScript, and .NET for refreshing access tokens using Microsoft Entra ID (formerly Azure AD), while connecting to Azure Database for PostgreSQL.
4 |
5 | ## Table of Contents
6 | - [Overview](#overview)
7 | - [Python Usage](#python-usage)
8 | - [Java Usage](#java-usage)
9 | - [JavaScript Usage](#javascript-usage)
10 | - [Dotnet Usage](#dotnet-usage)
11 |
12 | ## Overview
13 | Access tokens are essential for securely accessing protected resources in Microsoft Entra ID. However, since they expire after a set duration, applications need a reliable refresh mechanism to maintain seamless authentication without interrupting the user experience.
14 | To support this, we've created extension methods for Npgsql (for .NET), psycopg (for Python), and JDBC/Hibernate (for Java). These methods can be easily invoked in your application code to handle token refresh logic, making it simpler to maintain secure and uninterrupted database connections.
15 |
16 | ## Python Usage
17 |
18 | This repository provides Entra ID authentication samples for three popular Python PostgreSQL libraries:
19 |
20 | - **psycopg2** - Legacy psycopg library (synchronous only)
21 | - **psycopg3** - Modern psycopg library with both synchronous and asynchronous support
22 | - **SQLAlchemy** - High-level ORM/database toolkit with both synchronous and asynchronous support
23 |
24 | Each implementation is located in its respective folder under `python/` with its own `entra_connection.py` and `sample.py` files.
25 |
26 | Note: This section covers two related scenarios — running the included samples, and reusing the sample code in your own project. Running the samples requires installing all dependencies in `python/requirements.txt` and using the `.env` file in the `python/` folder. If you're copying sample code into your own project, you only need the parts you use (for example, just the `psycopg3/entra_connection.py` file) and can obtain connection details however your application does (configuration service, secrets manager, environment variables, etc.).
27 |
28 | ### Prerequisites
29 | - Python 3.8+
30 | - Install dependencies:
31 | ```powershell
32 | cd python
33 | pip install -r requirements.txt
34 | ```
35 |
36 | ### Environment Variables (.env)
37 |
38 | Create a `.env` file in the `python` folder with your database connection details:
39 |
40 | ```env
41 | HOSTNAME=
42 | DATABASE=
43 | ```
44 |
45 | Make sure to add `.env` to your `.gitignore` file to avoid committing secrets to source control.
46 |
47 | ### Psycopg2 Usage
48 |
49 | Run the sample:
50 | ```powershell
51 | cd python
52 | python psycopg2/sample.py
53 | ```
54 |
55 | See `python/psycopg2/sample.py` for complete runnable examples.
56 |
57 | ### Psycopg3 Usage
58 |
59 | Run the sample:
60 | ```powershell
61 | cd python
62 | # Run both sync and async examples
63 | python psycopg3/sample.py
64 |
65 | # Or run only sync
66 | python psycopg3/sample.py --mode sync
67 |
68 | # Or run only async
69 | python psycopg3/sample.py --mode async
70 | ```
71 |
72 | See `python/psycopg3/sample.py` for complete runnable examples.
73 |
74 | ### SQLAlchemy Usage
75 |
76 | Run the sample:
77 | ```powershell
78 | cd python
79 | # Run both sync and async examples
80 | python sqlalchemy/sample.py
81 |
82 | # Or run only sync
83 | python sqlalchemy/sample.py --mode sync
84 |
85 | # Or run only async
86 | python sqlalchemy/sample.py --mode async
87 | ```
88 |
89 | See `python/sqlalchemy/sample.py` for complete runnable examples.
90 |
91 | ### Using in Your Own Project
92 |
93 | To integrate Entra ID authentication into your own Python project, follow these steps:
94 |
95 | #### For psycopg2
96 |
97 | 1. **Copy the helper module:**
98 |
99 | Copy `python/psycopg2/entra_connection.py`, `python/shared.py`, and `python/errors.py` from this repository into your project.
100 |
101 | 2. **Install required dependencies:**
102 |
103 | ```bash
104 | pip install psycopg2-binary azure-identity
105 | ```
106 |
107 | 3. **Import and use in your code:**
108 |
109 | ```python
110 | from entra_connection import EntraConnection
111 |
112 | # Create a connection with Entra ID authentication
113 | conn = EntraConnection.connect(
114 | host='your-server.postgres.database.azure.com',
115 | dbname='your-database',
116 | sslmode='require'
117 | )
118 |
119 | # Use the connection
120 | with conn.cursor() as cur:
121 | cur.execute("SELECT current_user")
122 | print(cur.fetchone())
123 |
124 | conn.close()
125 | ```
126 |
127 | #### For psycopg3
128 |
129 | 1. **Copy the helper modules:**
130 |
131 | Copy `python/psycopg3/entra_connection.py`, `python/psycopg3/async_entra_connection.py`, `python/shared.py`, and `python/errors.py` from this repository into your project.
132 |
133 | 2. **Install required dependencies:**
134 |
135 | ```bash
136 | pip install psycopg[binary] psycopg-pool azure-identity
137 | ```
138 |
139 | 3. **Import and use in your code:**
140 |
141 | **Synchronous:**
142 | ```python
143 | from entra_connection import EntraConnection
144 | from psycopg_pool import ConnectionPool
145 |
146 | pool = ConnectionPool(
147 | conninfo="postgresql://your-server.postgres.database.azure.com:5432/your-database",
148 | min_size=1,
149 | max_size=5,
150 | connection_class=EntraConnection,
151 | kwargs=dict(sslmode="require")
152 | )
153 |
154 | # Use the pool
155 | with pool.connection() as conn:
156 | with conn.cursor() as cur:
157 | cur.execute("SELECT current_user")
158 | print(cur.fetchone())
159 | ```
160 |
161 | **Asynchronous:**
162 | ```python
163 | import asyncio
164 | from async_entra_connection import AsyncEntraConnection
165 | from psycopg_pool import AsyncConnectionPool
166 |
167 | async def main():
168 | pool = AsyncConnectionPool(
169 | conninfo="postgresql://your-server.postgres.database.azure.com:5432/your-database",
170 | min_size=1,
171 | max_size=5,
172 | connection_class=AsyncEntraConnection,
173 | kwargs=dict(sslmode="require")
174 | )
175 |
176 | async with pool.connection() as conn:
177 | async with conn.cursor() as cur:
178 | await cur.execute("SELECT current_user")
179 | print(await cur.fetchone())
180 |
181 | await pool.close()
182 |
183 | asyncio.run(main())
184 | ```
185 |
186 | #### For SQLAlchemy
187 |
188 | 1. **Copy the helper modules:**
189 |
190 | Copy `python/sqlalchemy/entra_connection.py`, `python/sqlalchemy/async_entra_connection.py`, `python/shared.py`, and `python/errors.py` from this repository into your project.
191 |
192 | 2. **Install required dependencies:**
193 |
194 | ```bash
195 | pip install sqlalchemy psycopg[binary] azure-identity
196 | ```
197 |
198 | 3. **Import and use in your code:**
199 |
200 | **Synchronous:**
201 | ```python
202 | from sqlalchemy import create_engine, text
203 | from entra_connection import enable_entra_authentication
204 |
205 | engine = create_engine("postgresql+psycopg://your-server.postgres.database.azure.com:5432/your-database")
206 | enable_entra_authentication(engine)
207 |
208 | # Use the engine
209 | with engine.connect() as conn:
210 | result = conn.execute(text("SELECT current_user"))
211 | print(result.fetchone())
212 | ```
213 |
214 | **Asynchronous:**
215 | ```python
216 | import asyncio
217 | from sqlalchemy.ext.asyncio import create_async_engine
218 | from sqlalchemy import text
219 | from async_entra_connection import enable_entra_authentication_async
220 |
221 | async def main():
222 | engine = create_async_engine("postgresql+psycopg://your-server.postgres.database.azure.com:5432/your-database")
223 | enable_entra_authentication_async(engine)
224 |
225 | async with engine.connect() as conn:
226 | result = await conn.execute(text("SELECT current_user"))
227 | print(result.fetchone())
228 |
229 | await engine.dispose()
230 |
231 | asyncio.run(main())
232 | ```
233 |
234 | #### Configure Authentication
235 |
236 | Ensure your application has access to Azure credentials through one of these methods:
237 | - **Azure CLI**: Run `az login` before running your app
238 | - **Environment variables**: Set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`
239 | - **Managed Identity**: When running on Azure (App Service, VM, Container Apps, etc.)
240 | - **VS Code**: Sign in to Azure extension
241 | - **Other**: Any method supported by `DefaultAzureCredential`
242 |
243 | The helper modules handle token acquisition, automatic refresh, and username extraction from JWT claims. You don't need to modify them—just import and use as shown above.
244 |
245 | ### How Token Refresh Works (Python)
246 |
247 | All three Python implementations use Azure Identity's `TokenCredential` (by default `DefaultAzureCredential`) to acquire Entra ID access tokens scoped for Azure Database for PostgreSQL. The tokens are used as the database password for authentication.
248 |
249 | #### psycopg2 Implementation
250 |
251 | The `EntraConnection` class extends psycopg2's `connection` class:
252 |
253 | - Overrides the `connect()` method to fetch Entra ID tokens before establishing the connection
254 | - Uses synchronous token acquisition via `get_entra_conninfo()`
255 | - Parses token claims to extract the database username from `upn`, `preferred_username`, or other claims
256 | - Each new connection automatically gets a fresh token
257 |
258 | #### psycopg3 Implementation
259 |
260 | psycopg3 provides both synchronous and asynchronous connection classes:
261 |
262 | - **`EntraConnection`**: Synchronous connections using `get_entra_conninfo()`
263 | - **`AsyncEntraConnection`**: Asynchronous connections using `get_entra_conninfo_async()`
264 | - Both classes integrate with psycopg's connection pools via the `connection_class` parameter
265 | - Tokens are acquired on-demand for each new connection, avoiding separate refresh threads
266 | - Username extraction follows the same claim hierarchy as psycopg2
267 |
268 | #### SQLAlchemy Implementation
269 |
270 | SQLAlchemy uses event listeners to inject Entra authentication:
271 |
272 | - **`enable_entra_authentication(engine)`**: For synchronous SQLAlchemy engines
273 | - **`enable_entra_authentication_async(engine)`**: For asynchronous SQLAlchemy engines
274 | - Registers a `do_connect` event handler that runs before each connection is established
275 | - The event handler fetches fresh tokens and injects them as connection parameters
276 | - Works with any SQLAlchemy-compatible PostgreSQL driver (uses psycopg by default)
277 |
278 | **Key Benefits Across All Implementations:**
279 |
280 | - Automatic token refresh on each connection (tokens expire after ~1 hour)
281 | - No manual token management or refresh threads required
282 | - Seamless integration with connection pooling
283 | - Works with `DefaultAzureCredential` for automatic credential discovery (Managed Identity, Azure CLI, etc.)
284 |
285 | Example token refresh flow:
286 |
287 | 1. Application requests a database connection from the pool
288 | 2. The custom connection class or event handler intercepts the connection creation
289 | 3. A fresh Entra ID token is requested from Azure Identity
290 | 4. The token is used as the password for PostgreSQL authentication
291 | 5. Connection is established and returned to the application
292 |
293 | ## Java Usage
294 |
295 | This repository provides Entra ID authentication samples for Java applications using PostgreSQL, with support for both plain JDBC and Hibernate ORM.
296 |
297 | ### Prerequisites
298 | - Java 17 or higher
299 | - Maven 3.6+ (for dependency management)
300 | - Azure Identity Extensions library
301 |
302 | ### Setup
303 |
304 | 1. **Install Maven dependencies:**
305 |
306 | The project includes a `pom.xml` file with all required dependencies. Navigate to the `java` folder and run:
307 |
308 | ```powershell
309 | cd java
310 | mvn clean compile
311 |
312 | 2. **Configure database connection:**
313 |
314 | Create or edit `application.properties` in the `java` folder:
315 |
316 | ```properties
317 | url=jdbc:postgresql://.postgres.database.azure.com:5432/?sslmode=require&authenticationPluginClassName=com.azure.identity.extensions.jdbc.postgresql.AzurePostgresqlAuthenticationPlugin
318 | user=@.onmicrosoft.com
319 |
320 | ### Running the Examples
321 |
322 | The project includes two examples:
323 | - `EntraIdExtensionJdbc.java` - Basic JDBC and HikariCP connection pooling
324 | - `EntraIdExtensionHibernate.java` - Hibernate ORM with Entra ID authentication
325 |
326 | **To switch between examples**, edit the `` property in `pom.xml`:
327 |
328 | ```xml
329 |
330 | org.codehaus.mojo
331 | exec-maven-plugin
332 | 3.1.0
333 |
334 |
335 | EntraIdExtensionJdbc
336 |
337 |
338 |
339 | ```
340 |
341 | Then run:
342 | ```powershell
343 | cd java
344 | mvn exec:java
345 | ```
346 |
347 | **Note:** Do not use VS Code's "Run" button directly. Run examples through Maven to ensure proper classpath and resource loading.
348 |
349 | ### Using in Your Own Project
350 |
351 | To integrate Entra ID authentication into your own Java project, you can follow the same setup steps for running the examples.
352 |
353 | ### How Token Refresh Works (Java)
354 |
355 | The Azure Identity Extensions library (`azure-identity-extensions`) automatically handles token refresh:
356 |
357 | 1. **Authentication Plugin**: The JDBC URL includes `authenticationPluginClassName=com.azure.identity.extensions.jdbc.postgresql.AzurePostgresqlAuthenticationPlugin` which intercepts connection attempts.
358 |
359 | 2. **Token Acquisition**: The plugin uses `DefaultAzureCredential` to automatically acquire Entra ID access tokens scoped for Azure Database for PostgreSQL.
360 |
361 | 3. **Automatic Refresh**:
362 | - For single connections: A fresh token is acquired for each connection
363 | - For connection pools: Tokens are refreshed automatically when connections are created or revalidated
364 | - The `maxLifetime` setting in HikariCP (30 minutes) ensures connections are recycled before token expiration
365 |
366 | 4. **Credential Discovery**: DefaultAzureCredential attempts authentication in this order:
367 | - Environment variables
368 | - Managed Identity
369 | - Azure CLI credentials
370 | - IntelliJ credentials
371 | - VS Code credentials
372 | - And more...
373 |
374 | This design ensures tokens are always valid without manual refresh logic, and connection pools automatically handle token lifecycle.
375 |
376 | ## JavaScript Usage
377 |
378 | This repository provides Entra ID authentication samples for JavaScript/Node.js applications using PostgreSQL, with support for both the `pg` (node-postgres) library and Sequelize ORM.
379 |
380 | ### Prerequisites
381 | - Node.js 18+ (for ES modules support)
382 | - npm or yarn package manager
383 |
384 | ### Setup
385 |
386 | 1. **Install dependencies:**
387 |
388 | Navigate to the `javascript` folder and install required packages:
389 |
390 | ```bash
391 | cd javascript
392 | npm install
393 | ```
394 |
395 | 2. **Configure database connection:**
396 |
397 | Create a `.env` file in the `javascript` folder:
398 |
399 | ```env
400 | PGHOST=.postgres.database.azure.com
401 | PGPORT=5432
402 | PGDATABASE=
403 | PGUSER=@.onmicrosoft.com
404 | ```
405 |
406 | Replace:
407 | - `` with your Azure PostgreSQL server hostname
408 | - `` with your database name
409 | - `` with your database name
410 | - `@.onmicrosoft.com` with your Entra ID user principal name
411 |
412 | ### Running the Examples
413 |
414 | **pg (node-postgres) Example:**
415 | ```bash
416 | cd javascript
417 | npm run pg
418 | ```
419 |
420 | **Sequelize ORM Example:**
421 | ```bash
422 | cd javascript
423 | npm run sequelize
424 | ```
425 |
426 | ### Using in Your Own Project
427 |
428 | To integrate Entra ID authentication into your own JavaScript/Node.js project, follow these steps:
429 |
430 | 1. **Copy the helper module:**
431 |
432 | Copy `javascript/entra-connection.js` from this repository into your project.
433 |
434 | 2. **Install required dependencies:**
435 |
436 | ```bash
437 | npm install pg sequelize @azure/identity
438 | ```
439 |
440 | Note: Install only the libraries you need (`pg` and/or `sequelize`).
441 |
442 | 3. **Import and use in your code:**
443 |
444 | **For pg (node-postgres):**
445 | ```javascript
446 | import pg from "pg";
447 | import { getEntraTokenPassword } from './entra-connection.js';
448 |
449 | const { Pool } = pg;
450 |
451 | const pool = new Pool({
452 | host: 'your-server.postgres.database.azure.com',
453 | port: 5432,
454 | database: 'your-database',
455 | user: 'your-user@yourdomain.onmicrosoft.com',
456 | password: getEntraTokenPassword, // Function callback
457 | ssl: { rejectUnauthorized: false }
458 | });
459 | ```
460 |
461 | **For Sequelize:**
462 | ```javascript
463 | import { Sequelize } from 'sequelize';
464 | import { configureEntraIdAuth } from './entra-connection.js';
465 |
466 | const sequelize = new Sequelize({
467 | dialect: 'postgres',
468 | host: 'your-server.postgres.database.azure.com',
469 | port: 5432,
470 | database: 'your-database',
471 | dialectOptions: {
472 | ssl: { rejectUnauthorized: false }
473 | }
474 | });
475 |
476 | // Enable automatic token refresh
477 | configureEntraIdAuth(sequelize);
478 | ```
479 |
480 | 4. **Configure authentication:**
481 |
482 | Ensure your application has access to Azure credentials through one of these methods:
483 | - Azure CLI: Run `az login` before running your app
484 | - Environment variables: Set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`
485 | - Managed Identity: When running on Azure (App Service, VM, etc.)
486 | - VS Code: Sign in to Azure extension
487 |
488 | The `entra-connection.js` module handles token acquisition, caching, and username extraction from JWT claims. You don't need to modify it—just import and use the functions as shown above.
489 |
490 | ### How Token Refresh Works (JavaScript)
491 |
492 | The `entra-connection.js` module provides helper functions for Entra ID authentication:
493 |
494 | 1. **`getEntraTokenPassword()`**: Acquires an access token using `DefaultAzureCredential` from `@azure/identity`. This token is scoped for Azure Database for PostgreSQL (`https://ossrdbms-aad.database.windows.net/.default`).
495 |
496 | 2. **`configureEntraIdAuth(sequelizeInstance)`**: Configures Sequelize to automatically fetch fresh tokens before each connection using the `beforeConnect` hook. This ensures:
497 | - A fresh token is acquired for every new database connection
498 | - The token is injected as the password
499 | - The username is derived from token claims (upn, appid) if needed
500 |
501 | 3. **Token Lifecycle**:
502 | - **pg example**: Pass `getEntraTokenPassword` as a callback to the `password` field. The `pg` library will call this function dynamically each time a new connection is established, ensuring fresh tokens are always used.
503 | - **Sequelize example**: Token is refreshed automatically before each connection via the `beforeConnect` hook.
504 |
505 | 4. **Credential Discovery**: `DefaultAzureCredential` attempts authentication in this order:
506 | - Environment variables
507 | - Managed Identity
508 | - Azure CLI credentials
509 | - IntelliJ credentials
510 | - VS Code credentials
511 | - And more...
512 |
513 | This design ensures tokens are always valid without manual refresh logic, and connection pools automatically handle token lifecycle.
514 |
515 | ## Dotnet Usage
516 |
517 | ### Prerequisites
518 | - .NET 8.0 or 9.0 SDK installed
519 | - Optional: .NET 10.0 SDK if you want to build/run for `net10.0`
520 | - Npgsql (for PostgreSQL integration, if needed)
521 |
522 | ### Example Usage
523 | 1. Copy `appsettings.example.json` to `appsettings.json` and fill in your credentials:
524 | 2. Build the project:
525 |
526 | ```powershell
527 | cd dotnet
528 | # Build for default frameworks (net8.0 and net9.0)
529 | dotnet build
530 |
531 | # Optionally include net10.0 (requires .NET 10 SDK):
532 | dotnet build -p:IncludeNet10=true
533 | ```
534 | 3. Run the sample:
535 |
536 | ```powershell
537 | # Run for .NET 8.0
538 | dotnet run --project PGEntraExamples.csproj -f net8.0
539 |
540 | # Or run for .NET 9.0
541 | dotnet run --project PGEntraExamples.csproj -f net9.0
542 |
543 | # Or (if built with .NET 10 SDK) run for .NET 10.0
544 | dotnet run --project PGEntraExamples.csproj -f net10.0
545 | ```
546 | 4. Use the logic in `NpgsqlDataSourceBuilderExtensions.cs` and `sample.cs` to handle token refresh.
547 |
548 | ### Environment Variables (.env)
549 |
550 | You can also use environment variables instead of or alongside `appsettings.json`:
551 |
552 | ```powershell
553 | # Set environment variables (PowerShell)
554 | $env:Host = "your-server.postgres.database.azure.com"
555 | $env:Database = "mydatabase"
556 | $env:Port = "5432"
557 | $env:SslMode = "Require"
558 |
559 | # Then run the sample
560 | dotnet run
561 | ```
562 |
563 | ```bash
564 | # Or in bash/Linux
565 | export Host="your-server.postgres.database.azure.com"
566 | export Database="mydatabase"
567 | export Port="5432"
568 | export SslMode="Require"
569 |
570 | dotnet run
571 | ```
572 |
573 | The sample will automatically read from both `appsettings.json` and environment variables, with environment variables taking precedence.
574 |
575 | ### Using in Your Own Project
576 |
577 | To integrate Entra ID authentication into your own .NET project, follow these steps:
578 |
579 | 1. **Copy the helper module:**
580 |
581 | Copy `dotnet/NpgsqlDataSourceBuilderExtensions.cs` from this repository into your project.
582 |
583 | 2. **Install required dependencies:**
584 |
585 | ```bash
586 | dotnet add package Npgsql
587 | dotnet add package Azure.Identity
588 | ```
589 |
590 | 3. **Import and use in your code:**
591 |
592 | **Synchronous approach:**
593 | ```csharp
594 | using Npgsql;
595 | using Azure.Identity;
596 |
597 | var connectionString = "Host=your-server.postgres.database.azure.com;Port=5432;Database=your-database;SSL Mode=Require";
598 |
599 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
600 |
601 | // Enable Entra ID authentication
602 | dataSourceBuilder.UseEntraAuthentication();
603 |
604 | await using var dataSource = dataSourceBuilder.Build();
605 | await using var connection = await dataSource.OpenConnectionAsync();
606 |
607 | // Use the connection
608 | await using var cmd = new NpgsqlCommand("SELECT current_user", connection);
609 | var user = await cmd.ExecuteScalarAsync();
610 | Console.WriteLine($"Connected as: {user}");
611 | ```
612 |
613 | **Asynchronous approach:**
614 | ```csharp
615 | using Npgsql;
616 | using Azure.Identity;
617 |
618 | var connectionString = "Host=your-server.postgres.database.azure.com;Port=5432;Database=your-database;SSL Mode=Require";
619 |
620 | var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
621 |
622 | // Enable Entra ID authentication with async token acquisition
623 | await dataSourceBuilder.UseEntraAuthenticationAsync();
624 |
625 | await using var dataSource = dataSourceBuilder.Build();
626 | await using var connection = await dataSource.OpenConnectionAsync();
627 |
628 | // Use the connection
629 | await using var cmd = new NpgsqlCommand("SELECT current_user", connection);
630 | var user = await cmd.ExecuteScalarAsync();
631 | Console.WriteLine($"Connected as: {user}");
632 | ```
633 |
634 | 4. **Configure authentication:**
635 |
636 | Ensure your application has access to Azure credentials through one of these methods:
637 | - **Azure CLI**: Run `az login` before running your app (local development)
638 | - **Environment variables**: Set `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`
639 | - **Managed Identity**: When running on Azure (App Service, Container Apps, VM, AKS, etc.)
640 | - **Visual Studio / VS Code**: Sign in to Azure
641 | - **Other**: Any method supported by `DefaultAzureCredential`
642 |
643 | The `NpgsqlDataSourceBuilderExtensions` class handles token acquisition, automatic refresh, and username extraction from JWT claims. You don't need to modify it—just use the extension methods as shown above.
644 |
645 | ### How Token Refresh is Implemented in .NET
646 |
647 | The extension method `UseEntraAuthentication` for `NpgsqlDataSourceBuilder` configures PostgreSQL connections to use Entra ID authentication. Here’s how it works:
648 |
649 | - **Token Acquisition:**
650 | - Uses `TokenCredential` (defaulting to `DefaultAzureCredential`) to acquire an access token for Azure DB for PostgreSQL.
651 | - If the connection string does not specify a username, it extracts the username from the token claims (`upn`, `preferred_username`, or `unique_name`).
652 | - **Password Provider:**
653 | - Sets a password provider on the builder that fetches a fresh token each time a password is needed, ensuring the token is always valid.
654 | - Supports both synchronous and asynchronous token acquisition via `GetToken` and `GetTokenAsync`.
655 | - **Async Support:**
656 | - The async extension `UseEntraAuthenticationAsync` allows for non-blocking token acquisition and connection setup, ideal for scalable applications.
657 | - **JWT Parsing:**
658 | - Decodes the JWT token payload to extract the username claim, handling base64 padding and JSON parsing.
659 |
660 | This approach ensures secure, up-to-date authentication for every database connection, with minimal configuration required by the user.
661 |
662 | ## Next Steps
663 | For further details, see the code comments and sample files in each language folder.
664 |
--------------------------------------------------------------------------------
/javascript/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "postgres-entra-sample",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "postgres-entra-sample",
9 | "version": "1.0.0",
10 | "dependencies": {
11 | "@azure/identity": "^4.0.0",
12 | "dotenv": "^16.4.5",
13 | "pg": "^8.11.3",
14 | "sequelize": "^6.35.2"
15 | }
16 | },
17 | "node_modules/@azure/abort-controller": {
18 | "version": "2.1.2",
19 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
20 | "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
21 | "license": "MIT",
22 | "dependencies": {
23 | "tslib": "^2.6.2"
24 | },
25 | "engines": {
26 | "node": ">=18.0.0"
27 | }
28 | },
29 | "node_modules/@azure/core-auth": {
30 | "version": "1.10.1",
31 | "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz",
32 | "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==",
33 | "license": "MIT",
34 | "dependencies": {
35 | "@azure/abort-controller": "^2.1.2",
36 | "@azure/core-util": "^1.13.0",
37 | "tslib": "^2.6.2"
38 | },
39 | "engines": {
40 | "node": ">=20.0.0"
41 | }
42 | },
43 | "node_modules/@azure/core-client": {
44 | "version": "1.10.1",
45 | "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz",
46 | "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
47 | "license": "MIT",
48 | "dependencies": {
49 | "@azure/abort-controller": "^2.1.2",
50 | "@azure/core-auth": "^1.10.0",
51 | "@azure/core-rest-pipeline": "^1.22.0",
52 | "@azure/core-tracing": "^1.3.0",
53 | "@azure/core-util": "^1.13.0",
54 | "@azure/logger": "^1.3.0",
55 | "tslib": "^2.6.2"
56 | },
57 | "engines": {
58 | "node": ">=20.0.0"
59 | }
60 | },
61 | "node_modules/@azure/core-rest-pipeline": {
62 | "version": "1.22.1",
63 | "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.1.tgz",
64 | "integrity": "sha512-UVZlVLfLyz6g3Hy7GNDpooMQonUygH7ghdiSASOOHy97fKj/mPLqgDX7aidOijn+sCMU+WU8NjlPlNTgnvbcGA==",
65 | "license": "MIT",
66 | "dependencies": {
67 | "@azure/abort-controller": "^2.1.2",
68 | "@azure/core-auth": "^1.10.0",
69 | "@azure/core-tracing": "^1.3.0",
70 | "@azure/core-util": "^1.13.0",
71 | "@azure/logger": "^1.3.0",
72 | "@typespec/ts-http-runtime": "^0.3.0",
73 | "tslib": "^2.6.2"
74 | },
75 | "engines": {
76 | "node": ">=20.0.0"
77 | }
78 | },
79 | "node_modules/@azure/core-tracing": {
80 | "version": "1.3.1",
81 | "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz",
82 | "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==",
83 | "license": "MIT",
84 | "dependencies": {
85 | "tslib": "^2.6.2"
86 | },
87 | "engines": {
88 | "node": ">=20.0.0"
89 | }
90 | },
91 | "node_modules/@azure/core-util": {
92 | "version": "1.13.1",
93 | "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz",
94 | "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==",
95 | "license": "MIT",
96 | "dependencies": {
97 | "@azure/abort-controller": "^2.1.2",
98 | "@typespec/ts-http-runtime": "^0.3.0",
99 | "tslib": "^2.6.2"
100 | },
101 | "engines": {
102 | "node": ">=20.0.0"
103 | }
104 | },
105 | "node_modules/@azure/identity": {
106 | "version": "4.13.0",
107 | "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz",
108 | "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==",
109 | "license": "MIT",
110 | "dependencies": {
111 | "@azure/abort-controller": "^2.0.0",
112 | "@azure/core-auth": "^1.9.0",
113 | "@azure/core-client": "^1.9.2",
114 | "@azure/core-rest-pipeline": "^1.17.0",
115 | "@azure/core-tracing": "^1.0.0",
116 | "@azure/core-util": "^1.11.0",
117 | "@azure/logger": "^1.0.0",
118 | "@azure/msal-browser": "^4.2.0",
119 | "@azure/msal-node": "^3.5.0",
120 | "open": "^10.1.0",
121 | "tslib": "^2.2.0"
122 | },
123 | "engines": {
124 | "node": ">=20.0.0"
125 | }
126 | },
127 | "node_modules/@azure/logger": {
128 | "version": "1.3.0",
129 | "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
130 | "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==",
131 | "license": "MIT",
132 | "dependencies": {
133 | "@typespec/ts-http-runtime": "^0.3.0",
134 | "tslib": "^2.6.2"
135 | },
136 | "engines": {
137 | "node": ">=20.0.0"
138 | }
139 | },
140 | "node_modules/@azure/msal-browser": {
141 | "version": "4.25.1",
142 | "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.25.1.tgz",
143 | "integrity": "sha512-kAdOSNjvMbeBmEyd5WnddGmIpKCbAAGj4Gg/1iURtF+nHmIfS0+QUBBO3uaHl7CBB2R1SEAbpOgxycEwrHOkFA==",
144 | "license": "MIT",
145 | "dependencies": {
146 | "@azure/msal-common": "15.13.0"
147 | },
148 | "engines": {
149 | "node": ">=0.8.0"
150 | }
151 | },
152 | "node_modules/@azure/msal-common": {
153 | "version": "15.13.0",
154 | "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz",
155 | "integrity": "sha512-8oF6nj02qX7eE/6+wFT5NluXRHc05AgdCC3fJnkjiJooq8u7BcLmxaYYSwc2AfEkWRMRi6Eyvvbeqk4U4412Ag==",
156 | "license": "MIT",
157 | "engines": {
158 | "node": ">=0.8.0"
159 | }
160 | },
161 | "node_modules/@azure/msal-node": {
162 | "version": "3.8.0",
163 | "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.0.tgz",
164 | "integrity": "sha512-23BXm82Mp5XnRhrcd4mrHa0xuUNRp96ivu3nRatrfdAqjoeWAGyD0eEAafxAOHAEWWmdlyFK4ELFcdziXyw2sA==",
165 | "license": "MIT",
166 | "dependencies": {
167 | "@azure/msal-common": "15.13.0",
168 | "jsonwebtoken": "^9.0.0",
169 | "uuid": "^8.3.0"
170 | },
171 | "engines": {
172 | "node": ">=16"
173 | }
174 | },
175 | "node_modules/@types/debug": {
176 | "version": "4.1.12",
177 | "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
178 | "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
179 | "license": "MIT",
180 | "dependencies": {
181 | "@types/ms": "*"
182 | }
183 | },
184 | "node_modules/@types/ms": {
185 | "version": "2.1.0",
186 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
187 | "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
188 | "license": "MIT"
189 | },
190 | "node_modules/@types/node": {
191 | "version": "24.9.1",
192 | "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
193 | "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
194 | "license": "MIT",
195 | "dependencies": {
196 | "undici-types": "~7.16.0"
197 | }
198 | },
199 | "node_modules/@types/validator": {
200 | "version": "13.15.3",
201 | "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz",
202 | "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==",
203 | "license": "MIT"
204 | },
205 | "node_modules/@typespec/ts-http-runtime": {
206 | "version": "0.3.1",
207 | "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.1.tgz",
208 | "integrity": "sha512-SnbaqayTVFEA6/tYumdF0UmybY0KHyKwGPBXnyckFlrrKdhWFrL3a2HIPXHjht5ZOElKGcXfD2D63P36btb+ww==",
209 | "license": "MIT",
210 | "dependencies": {
211 | "http-proxy-agent": "^7.0.0",
212 | "https-proxy-agent": "^7.0.0",
213 | "tslib": "^2.6.2"
214 | },
215 | "engines": {
216 | "node": ">=20.0.0"
217 | }
218 | },
219 | "node_modules/agent-base": {
220 | "version": "7.1.4",
221 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
222 | "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
223 | "license": "MIT",
224 | "engines": {
225 | "node": ">= 14"
226 | }
227 | },
228 | "node_modules/buffer-equal-constant-time": {
229 | "version": "1.0.1",
230 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
231 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
232 | "license": "BSD-3-Clause"
233 | },
234 | "node_modules/bundle-name": {
235 | "version": "4.1.0",
236 | "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
237 | "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
238 | "license": "MIT",
239 | "dependencies": {
240 | "run-applescript": "^7.0.0"
241 | },
242 | "engines": {
243 | "node": ">=18"
244 | },
245 | "funding": {
246 | "url": "https://github.com/sponsors/sindresorhus"
247 | }
248 | },
249 | "node_modules/debug": {
250 | "version": "4.4.3",
251 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
252 | "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
253 | "license": "MIT",
254 | "dependencies": {
255 | "ms": "^2.1.3"
256 | },
257 | "engines": {
258 | "node": ">=6.0"
259 | },
260 | "peerDependenciesMeta": {
261 | "supports-color": {
262 | "optional": true
263 | }
264 | }
265 | },
266 | "node_modules/default-browser": {
267 | "version": "5.2.1",
268 | "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
269 | "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
270 | "license": "MIT",
271 | "dependencies": {
272 | "bundle-name": "^4.1.0",
273 | "default-browser-id": "^5.0.0"
274 | },
275 | "engines": {
276 | "node": ">=18"
277 | },
278 | "funding": {
279 | "url": "https://github.com/sponsors/sindresorhus"
280 | }
281 | },
282 | "node_modules/default-browser-id": {
283 | "version": "5.0.0",
284 | "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
285 | "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
286 | "license": "MIT",
287 | "engines": {
288 | "node": ">=18"
289 | },
290 | "funding": {
291 | "url": "https://github.com/sponsors/sindresorhus"
292 | }
293 | },
294 | "node_modules/define-lazy-prop": {
295 | "version": "3.0.0",
296 | "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
297 | "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
298 | "license": "MIT",
299 | "engines": {
300 | "node": ">=12"
301 | },
302 | "funding": {
303 | "url": "https://github.com/sponsors/sindresorhus"
304 | }
305 | },
306 | "node_modules/dotenv": {
307 | "version": "16.6.1",
308 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
309 | "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
310 | "license": "BSD-2-Clause",
311 | "engines": {
312 | "node": ">=12"
313 | },
314 | "funding": {
315 | "url": "https://dotenvx.com"
316 | }
317 | },
318 | "node_modules/dottie": {
319 | "version": "2.0.6",
320 | "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
321 | "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
322 | "license": "MIT"
323 | },
324 | "node_modules/ecdsa-sig-formatter": {
325 | "version": "1.0.11",
326 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
327 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
328 | "license": "Apache-2.0",
329 | "dependencies": {
330 | "safe-buffer": "^5.0.1"
331 | }
332 | },
333 | "node_modules/http-proxy-agent": {
334 | "version": "7.0.2",
335 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
336 | "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
337 | "license": "MIT",
338 | "dependencies": {
339 | "agent-base": "^7.1.0",
340 | "debug": "^4.3.4"
341 | },
342 | "engines": {
343 | "node": ">= 14"
344 | }
345 | },
346 | "node_modules/https-proxy-agent": {
347 | "version": "7.0.6",
348 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
349 | "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
350 | "license": "MIT",
351 | "dependencies": {
352 | "agent-base": "^7.1.2",
353 | "debug": "4"
354 | },
355 | "engines": {
356 | "node": ">= 14"
357 | }
358 | },
359 | "node_modules/inflection": {
360 | "version": "1.13.4",
361 | "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
362 | "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
363 | "engines": [
364 | "node >= 0.4.0"
365 | ],
366 | "license": "MIT"
367 | },
368 | "node_modules/is-docker": {
369 | "version": "3.0.0",
370 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
371 | "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
372 | "license": "MIT",
373 | "bin": {
374 | "is-docker": "cli.js"
375 | },
376 | "engines": {
377 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
378 | },
379 | "funding": {
380 | "url": "https://github.com/sponsors/sindresorhus"
381 | }
382 | },
383 | "node_modules/is-inside-container": {
384 | "version": "1.0.0",
385 | "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
386 | "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
387 | "license": "MIT",
388 | "dependencies": {
389 | "is-docker": "^3.0.0"
390 | },
391 | "bin": {
392 | "is-inside-container": "cli.js"
393 | },
394 | "engines": {
395 | "node": ">=14.16"
396 | },
397 | "funding": {
398 | "url": "https://github.com/sponsors/sindresorhus"
399 | }
400 | },
401 | "node_modules/is-wsl": {
402 | "version": "3.1.0",
403 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
404 | "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
405 | "license": "MIT",
406 | "dependencies": {
407 | "is-inside-container": "^1.0.0"
408 | },
409 | "engines": {
410 | "node": ">=16"
411 | },
412 | "funding": {
413 | "url": "https://github.com/sponsors/sindresorhus"
414 | }
415 | },
416 | "node_modules/jsonwebtoken": {
417 | "version": "9.0.2",
418 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
419 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
420 | "license": "MIT",
421 | "dependencies": {
422 | "jws": "^3.2.2",
423 | "lodash.includes": "^4.3.0",
424 | "lodash.isboolean": "^3.0.3",
425 | "lodash.isinteger": "^4.0.4",
426 | "lodash.isnumber": "^3.0.3",
427 | "lodash.isplainobject": "^4.0.6",
428 | "lodash.isstring": "^4.0.1",
429 | "lodash.once": "^4.0.0",
430 | "ms": "^2.1.1",
431 | "semver": "^7.5.4"
432 | },
433 | "engines": {
434 | "node": ">=12",
435 | "npm": ">=6"
436 | }
437 | },
438 | "node_modules/jwa": {
439 | "version": "1.4.2",
440 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
441 | "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
442 | "license": "MIT",
443 | "dependencies": {
444 | "buffer-equal-constant-time": "^1.0.1",
445 | "ecdsa-sig-formatter": "1.0.11",
446 | "safe-buffer": "^5.0.1"
447 | }
448 | },
449 | "node_modules/jws": {
450 | "version": "3.2.2",
451 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
452 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
453 | "license": "MIT",
454 | "dependencies": {
455 | "jwa": "^1.4.1",
456 | "safe-buffer": "^5.0.1"
457 | }
458 | },
459 | "node_modules/lodash": {
460 | "version": "4.17.21",
461 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
462 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
463 | "license": "MIT"
464 | },
465 | "node_modules/lodash.includes": {
466 | "version": "4.3.0",
467 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
468 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
469 | "license": "MIT"
470 | },
471 | "node_modules/lodash.isboolean": {
472 | "version": "3.0.3",
473 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
474 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
475 | "license": "MIT"
476 | },
477 | "node_modules/lodash.isinteger": {
478 | "version": "4.0.4",
479 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
480 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
481 | "license": "MIT"
482 | },
483 | "node_modules/lodash.isnumber": {
484 | "version": "3.0.3",
485 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
486 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
487 | "license": "MIT"
488 | },
489 | "node_modules/lodash.isplainobject": {
490 | "version": "4.0.6",
491 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
492 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
493 | "license": "MIT"
494 | },
495 | "node_modules/lodash.isstring": {
496 | "version": "4.0.1",
497 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
498 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
499 | "license": "MIT"
500 | },
501 | "node_modules/lodash.once": {
502 | "version": "4.1.1",
503 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
504 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
505 | "license": "MIT"
506 | },
507 | "node_modules/moment": {
508 | "version": "2.30.1",
509 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
510 | "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
511 | "license": "MIT",
512 | "engines": {
513 | "node": "*"
514 | }
515 | },
516 | "node_modules/moment-timezone": {
517 | "version": "0.5.48",
518 | "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
519 | "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
520 | "license": "MIT",
521 | "dependencies": {
522 | "moment": "^2.29.4"
523 | },
524 | "engines": {
525 | "node": "*"
526 | }
527 | },
528 | "node_modules/ms": {
529 | "version": "2.1.3",
530 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
531 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
532 | "license": "MIT"
533 | },
534 | "node_modules/open": {
535 | "version": "10.2.0",
536 | "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
537 | "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
538 | "license": "MIT",
539 | "dependencies": {
540 | "default-browser": "^5.2.1",
541 | "define-lazy-prop": "^3.0.0",
542 | "is-inside-container": "^1.0.0",
543 | "wsl-utils": "^0.1.0"
544 | },
545 | "engines": {
546 | "node": ">=18"
547 | },
548 | "funding": {
549 | "url": "https://github.com/sponsors/sindresorhus"
550 | }
551 | },
552 | "node_modules/pg": {
553 | "version": "8.16.3",
554 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
555 | "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
556 | "license": "MIT",
557 | "dependencies": {
558 | "pg-connection-string": "^2.9.1",
559 | "pg-pool": "^3.10.1",
560 | "pg-protocol": "^1.10.3",
561 | "pg-types": "2.2.0",
562 | "pgpass": "1.0.5"
563 | },
564 | "engines": {
565 | "node": ">= 16.0.0"
566 | },
567 | "optionalDependencies": {
568 | "pg-cloudflare": "^1.2.7"
569 | },
570 | "peerDependencies": {
571 | "pg-native": ">=3.0.1"
572 | },
573 | "peerDependenciesMeta": {
574 | "pg-native": {
575 | "optional": true
576 | }
577 | }
578 | },
579 | "node_modules/pg-cloudflare": {
580 | "version": "1.2.7",
581 | "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
582 | "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
583 | "license": "MIT",
584 | "optional": true
585 | },
586 | "node_modules/pg-connection-string": {
587 | "version": "2.9.1",
588 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
589 | "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
590 | "license": "MIT"
591 | },
592 | "node_modules/pg-int8": {
593 | "version": "1.0.1",
594 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
595 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
596 | "license": "ISC",
597 | "engines": {
598 | "node": ">=4.0.0"
599 | }
600 | },
601 | "node_modules/pg-pool": {
602 | "version": "3.10.1",
603 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
604 | "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
605 | "license": "MIT",
606 | "peerDependencies": {
607 | "pg": ">=8.0"
608 | }
609 | },
610 | "node_modules/pg-protocol": {
611 | "version": "1.10.3",
612 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
613 | "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
614 | "license": "MIT"
615 | },
616 | "node_modules/pg-types": {
617 | "version": "2.2.0",
618 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
619 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
620 | "license": "MIT",
621 | "dependencies": {
622 | "pg-int8": "1.0.1",
623 | "postgres-array": "~2.0.0",
624 | "postgres-bytea": "~1.0.0",
625 | "postgres-date": "~1.0.4",
626 | "postgres-interval": "^1.1.0"
627 | },
628 | "engines": {
629 | "node": ">=4"
630 | }
631 | },
632 | "node_modules/pgpass": {
633 | "version": "1.0.5",
634 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
635 | "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
636 | "license": "MIT",
637 | "dependencies": {
638 | "split2": "^4.1.0"
639 | }
640 | },
641 | "node_modules/postgres-array": {
642 | "version": "2.0.0",
643 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
644 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
645 | "license": "MIT",
646 | "engines": {
647 | "node": ">=4"
648 | }
649 | },
650 | "node_modules/postgres-bytea": {
651 | "version": "1.0.0",
652 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
653 | "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
654 | "license": "MIT",
655 | "engines": {
656 | "node": ">=0.10.0"
657 | }
658 | },
659 | "node_modules/postgres-date": {
660 | "version": "1.0.7",
661 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
662 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
663 | "license": "MIT",
664 | "engines": {
665 | "node": ">=0.10.0"
666 | }
667 | },
668 | "node_modules/postgres-interval": {
669 | "version": "1.2.0",
670 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
671 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
672 | "license": "MIT",
673 | "dependencies": {
674 | "xtend": "^4.0.0"
675 | },
676 | "engines": {
677 | "node": ">=0.10.0"
678 | }
679 | },
680 | "node_modules/retry-as-promised": {
681 | "version": "7.1.1",
682 | "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
683 | "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
684 | "license": "MIT"
685 | },
686 | "node_modules/run-applescript": {
687 | "version": "7.1.0",
688 | "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
689 | "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
690 | "license": "MIT",
691 | "engines": {
692 | "node": ">=18"
693 | },
694 | "funding": {
695 | "url": "https://github.com/sponsors/sindresorhus"
696 | }
697 | },
698 | "node_modules/safe-buffer": {
699 | "version": "5.2.1",
700 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
701 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
702 | "funding": [
703 | {
704 | "type": "github",
705 | "url": "https://github.com/sponsors/feross"
706 | },
707 | {
708 | "type": "patreon",
709 | "url": "https://www.patreon.com/feross"
710 | },
711 | {
712 | "type": "consulting",
713 | "url": "https://feross.org/support"
714 | }
715 | ],
716 | "license": "MIT"
717 | },
718 | "node_modules/semver": {
719 | "version": "7.7.3",
720 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
721 | "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
722 | "license": "ISC",
723 | "bin": {
724 | "semver": "bin/semver.js"
725 | },
726 | "engines": {
727 | "node": ">=10"
728 | }
729 | },
730 | "node_modules/sequelize": {
731 | "version": "6.37.7",
732 | "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
733 | "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
734 | "funding": [
735 | {
736 | "type": "opencollective",
737 | "url": "https://opencollective.com/sequelize"
738 | }
739 | ],
740 | "license": "MIT",
741 | "dependencies": {
742 | "@types/debug": "^4.1.8",
743 | "@types/validator": "^13.7.17",
744 | "debug": "^4.3.4",
745 | "dottie": "^2.0.6",
746 | "inflection": "^1.13.4",
747 | "lodash": "^4.17.21",
748 | "moment": "^2.29.4",
749 | "moment-timezone": "^0.5.43",
750 | "pg-connection-string": "^2.6.1",
751 | "retry-as-promised": "^7.0.4",
752 | "semver": "^7.5.4",
753 | "sequelize-pool": "^7.1.0",
754 | "toposort-class": "^1.0.1",
755 | "uuid": "^8.3.2",
756 | "validator": "^13.9.0",
757 | "wkx": "^0.5.0"
758 | },
759 | "engines": {
760 | "node": ">=10.0.0"
761 | },
762 | "peerDependenciesMeta": {
763 | "ibm_db": {
764 | "optional": true
765 | },
766 | "mariadb": {
767 | "optional": true
768 | },
769 | "mysql2": {
770 | "optional": true
771 | },
772 | "oracledb": {
773 | "optional": true
774 | },
775 | "pg": {
776 | "optional": true
777 | },
778 | "pg-hstore": {
779 | "optional": true
780 | },
781 | "snowflake-sdk": {
782 | "optional": true
783 | },
784 | "sqlite3": {
785 | "optional": true
786 | },
787 | "tedious": {
788 | "optional": true
789 | }
790 | }
791 | },
792 | "node_modules/sequelize-pool": {
793 | "version": "7.1.0",
794 | "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
795 | "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
796 | "license": "MIT",
797 | "engines": {
798 | "node": ">= 10.0.0"
799 | }
800 | },
801 | "node_modules/split2": {
802 | "version": "4.2.0",
803 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
804 | "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
805 | "license": "ISC",
806 | "engines": {
807 | "node": ">= 10.x"
808 | }
809 | },
810 | "node_modules/toposort-class": {
811 | "version": "1.0.1",
812 | "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
813 | "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
814 | "license": "MIT"
815 | },
816 | "node_modules/tslib": {
817 | "version": "2.8.1",
818 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
819 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
820 | "license": "0BSD"
821 | },
822 | "node_modules/undici-types": {
823 | "version": "7.16.0",
824 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
825 | "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
826 | "license": "MIT"
827 | },
828 | "node_modules/uuid": {
829 | "version": "8.3.2",
830 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
831 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
832 | "license": "MIT",
833 | "bin": {
834 | "uuid": "dist/bin/uuid"
835 | }
836 | },
837 | "node_modules/validator": {
838 | "version": "13.15.20",
839 | "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz",
840 | "integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==",
841 | "license": "MIT",
842 | "engines": {
843 | "node": ">= 0.10"
844 | }
845 | },
846 | "node_modules/wkx": {
847 | "version": "0.5.0",
848 | "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
849 | "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
850 | "license": "MIT",
851 | "dependencies": {
852 | "@types/node": "*"
853 | }
854 | },
855 | "node_modules/wsl-utils": {
856 | "version": "0.1.0",
857 | "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
858 | "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
859 | "license": "MIT",
860 | "dependencies": {
861 | "is-wsl": "^3.1.0"
862 | },
863 | "engines": {
864 | "node": ">=18"
865 | },
866 | "funding": {
867 | "url": "https://github.com/sponsors/sindresorhus"
868 | }
869 | },
870 | "node_modules/xtend": {
871 | "version": "4.0.2",
872 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
873 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
874 | "license": "MIT",
875 | "engines": {
876 | "node": ">=0.4"
877 | }
878 | }
879 | }
880 | }
881 |
--------------------------------------------------------------------------------