├── 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 | --------------------------------------------------------------------------------