|
| 1 | +# Azure PostgreSQL Auth client library for Python |
| 2 | + |
| 3 | +The Azure PostgreSQL Auth client library provides Microsoft Entra ID authentication for Python database drivers connecting to Azure Database for PostgreSQL. It supports psycopg2, psycopg3, and SQLAlchemy with automatic token management and connection pooling. |
| 4 | + |
| 5 | +[Source code](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/postgresql/azure-postgresql-auth) |
| 6 | +| [Package (PyPI)](https://pypi.org/project/azure-postgresql-auth/) |
| 7 | +| [Samples](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/postgresql/azure-postgresql-auth/samples) |
| 8 | + |
| 9 | +## Getting started |
| 10 | + |
| 11 | +### Prerequisites |
| 12 | + |
| 13 | +- Python 3.9 or later |
| 14 | +- An Azure subscription |
| 15 | +- An Azure Database for PostgreSQL Server instance with Entra ID authentication enabled |
| 16 | +- A credential object that implements the [TokenCredential](https://learn.microsoft.com/python/api/azure-core/azure.core.credentials.tokencredential) interface |
| 17 | + |
| 18 | +### Install the package |
| 19 | + |
| 20 | +Install the core package: |
| 21 | + |
| 22 | +```bash |
| 23 | +pip install azure-postgresql-auth |
| 24 | +``` |
| 25 | + |
| 26 | +Install with driver-specific extras: |
| 27 | + |
| 28 | +```bash |
| 29 | +# For psycopg3 (recommended for new projects) |
| 30 | +pip install "azure-postgresql-auth[psycopg3]" |
| 31 | + |
| 32 | +# For psycopg2 (legacy support) |
| 33 | +pip install "azure-postgresql-auth[psycopg2]" |
| 34 | + |
| 35 | +# For SQLAlchemy |
| 36 | +pip install "azure-postgresql-auth[sqlalchemy]" |
| 37 | +``` |
| 38 | + |
| 39 | +Install Azure Identity for credential support: |
| 40 | + |
| 41 | +```bash |
| 42 | +pip install azure-identity |
| 43 | +``` |
| 44 | + |
| 45 | +## Key concepts |
| 46 | + |
| 47 | +### Authentication flow |
| 48 | + |
| 49 | +1. **Token Acquisition**: Uses Azure Identity credentials to acquire access tokens from Microsoft Entra ID. |
| 50 | +2. **Automatic Refresh**: Tokens are acquired for each new database connection. |
| 51 | +3. **Secure Transport**: Tokens are passed as passwords in PostgreSQL connection strings over SSL. |
| 52 | +4. **Server Validation**: Azure Database for PostgreSQL validates the token and establishes the authenticated connection. |
| 53 | +5. **User Mapping**: The token's user principal name (UPN) is mapped to a PostgreSQL user for authorization. |
| 54 | + |
| 55 | +### Token scopes |
| 56 | + |
| 57 | +The library requests the following OAuth2 scopes: |
| 58 | + |
| 59 | +- **Database scope**: `https://ossrdbms-aad.database.windows.net/.default` (primary) |
| 60 | +- **Management scope**: `https://management.azure.com/.default` (fallback for managed identities) |
| 61 | + |
| 62 | +### Driver support |
| 63 | + |
| 64 | +- **psycopg3**: Modern PostgreSQL driver (recommended for new projects) — sync and async support |
| 65 | +- **psycopg2**: Legacy PostgreSQL driver — synchronous only |
| 66 | +- **SQLAlchemy**: ORM/Core interface using event listeners — sync and async engine support |
| 67 | + |
| 68 | +### Security features |
| 69 | + |
| 70 | +- **Token-based authentication**: No passwords stored or transmitted |
| 71 | +- **Automatic expiration**: Tokens expire and are refreshed automatically |
| 72 | +- **SSL enforcement**: All connections require SSL encryption |
| 73 | +- **Principle of least privilege**: Only database-specific scopes are requested |
| 74 | + |
| 75 | +## Examples |
| 76 | + |
| 77 | +### Configuration |
| 78 | + |
| 79 | +The samples use environment variables to configure database connections. Copy `.env.example` into a `.env` file |
| 80 | +in the same directory as the sample and update the variables: |
| 81 | + |
| 82 | +``` |
| 83 | +POSTGRES_SERVER=<your-server.postgres.database.azure.com> |
| 84 | +POSTGRES_DATABASE=<your_database_name> |
| 85 | +``` |
| 86 | + |
| 87 | +### psycopg2 — Connection pooling |
| 88 | + |
| 89 | +```python |
| 90 | +from azure_postgresql_auth.psycopg2 import EntraConnection |
| 91 | +from azure.identity import DefaultAzureCredential |
| 92 | +from psycopg2 import pool |
| 93 | +from functools import partial |
| 94 | + |
| 95 | +credential = DefaultAzureCredential() |
| 96 | +connection_factory = partial(EntraConnection, credential=credential) |
| 97 | + |
| 98 | +with pool.ThreadedConnectionPool( |
| 99 | + minconn=1, |
| 100 | + maxconn=5, |
| 101 | + host="your-server.postgres.database.azure.com", |
| 102 | + database="your_database", |
| 103 | + connection_factory=connection_factory, |
| 104 | +) as connection_pool: |
| 105 | + conn = connection_pool.getconn() |
| 106 | + with conn.cursor() as cur: |
| 107 | + cur.execute("SELECT 1") |
| 108 | +``` |
| 109 | + |
| 110 | +### psycopg2 — Direct connection |
| 111 | + |
| 112 | +```python |
| 113 | +from azure_postgresql_auth.psycopg2 import EntraConnection |
| 114 | +from azure.identity import DefaultAzureCredential |
| 115 | + |
| 116 | +with EntraConnection( |
| 117 | + "postgresql://your-server.postgres.database.azure.com:5432/your_database", |
| 118 | + credential=DefaultAzureCredential(), |
| 119 | +) as conn: |
| 120 | + with conn.cursor() as cur: |
| 121 | + cur.execute("SELECT 1") |
| 122 | +``` |
| 123 | + |
| 124 | +### psycopg3 — Synchronous connection |
| 125 | + |
| 126 | +```python |
| 127 | +from azure_postgresql_auth.psycopg3 import EntraConnection |
| 128 | +from azure.identity import DefaultAzureCredential |
| 129 | +from psycopg_pool import ConnectionPool |
| 130 | + |
| 131 | +with ConnectionPool( |
| 132 | + conninfo="postgresql://your-server.postgres.database.azure.com:5432/your_database", |
| 133 | + connection_class=EntraConnection, |
| 134 | + kwargs={"credential": DefaultAzureCredential()}, |
| 135 | + min_size=1, |
| 136 | + max_size=5, |
| 137 | +) as pg_pool: |
| 138 | + with pg_pool.connection() as conn: |
| 139 | + with conn.cursor() as cur: |
| 140 | + cur.execute("SELECT 1") |
| 141 | +``` |
| 142 | + |
| 143 | +### psycopg3 — Asynchronous connection |
| 144 | + |
| 145 | +```python |
| 146 | +from azure_postgresql_auth.psycopg3 import AsyncEntraConnection |
| 147 | +from azure.identity.aio import DefaultAzureCredential |
| 148 | +from psycopg_pool import AsyncConnectionPool |
| 149 | + |
| 150 | +async with AsyncConnectionPool( |
| 151 | + conninfo="postgresql://your-server.postgres.database.azure.com:5432/your_database", |
| 152 | + connection_class=AsyncEntraConnection, |
| 153 | + kwargs={"credential": DefaultAzureCredential()}, |
| 154 | + min_size=1, |
| 155 | + max_size=5, |
| 156 | +) as pg_pool: |
| 157 | + async with pg_pool.connection() as conn: |
| 158 | + async with conn.cursor() as cur: |
| 159 | + await cur.execute("SELECT 1") |
| 160 | +``` |
| 161 | + |
| 162 | +### SQLAlchemy — Synchronous engine |
| 163 | + |
| 164 | +> For more information, see SQLAlchemy's documentation on |
| 165 | +> [controlling how parameters are passed to the DBAPI connect function](https://docs.sqlalchemy.org/en/20/core/engines.html#controlling-how-parameters-are-passed-to-the-dbapi-connect-function). |
| 166 | +
|
| 167 | +```python |
| 168 | +from sqlalchemy import create_engine |
| 169 | +from azure_postgresql_auth.sqlalchemy import enable_entra_authentication |
| 170 | +from azure.identity import DefaultAzureCredential |
| 171 | + |
| 172 | +engine = create_engine( |
| 173 | + "postgresql+psycopg://your-server.postgres.database.azure.com/your_database", |
| 174 | + connect_args={"credential": DefaultAzureCredential()}, |
| 175 | +) |
| 176 | +enable_entra_authentication(engine) |
| 177 | + |
| 178 | +with engine.connect() as conn: |
| 179 | + result = conn.execute(text("SELECT 1")) |
| 180 | +``` |
| 181 | + |
| 182 | +### SQLAlchemy — Asynchronous engine |
| 183 | + |
| 184 | +```python |
| 185 | +from sqlalchemy.ext.asyncio import create_async_engine |
| 186 | +from azure_postgresql_auth.sqlalchemy import enable_entra_authentication_async |
| 187 | +from azure.identity import DefaultAzureCredential |
| 188 | + |
| 189 | +engine = create_async_engine( |
| 190 | + "postgresql+psycopg://your-server.postgres.database.azure.com/your_database", |
| 191 | + connect_args={"credential": DefaultAzureCredential()}, |
| 192 | +) |
| 193 | +enable_entra_authentication_async(engine) |
| 194 | + |
| 195 | +async with engine.connect() as conn: |
| 196 | + result = await conn.execute(text("SELECT 1")) |
| 197 | +``` |
| 198 | + |
| 199 | +## Troubleshooting |
| 200 | + |
| 201 | +### Authentication errors |
| 202 | + |
| 203 | +If you get "password authentication failed", ensure your Azure identity has been granted access to the database: |
| 204 | + |
| 205 | +```sql |
| 206 | +-- Run as a database administrator |
| 207 | +CREATE ROLE "your-user@your-domain.com" WITH LOGIN; |
| 208 | +GRANT ALL PRIVILEGES ON DATABASE your_database TO "your-user@your-domain.com"; |
| 209 | +``` |
| 210 | + |
| 211 | +### Connection timeouts |
| 212 | + |
| 213 | +Increase the connection timeout for slow networks: |
| 214 | + |
| 215 | +```python |
| 216 | +conn = EntraConnection.connect( |
| 217 | + "postgresql://server:5432/db", |
| 218 | + credential=DefaultAzureCredential(), |
| 219 | + connect_timeout=30, |
| 220 | +) |
| 221 | +``` |
| 222 | + |
| 223 | +### Windows async compatibility |
| 224 | + |
| 225 | +On Windows, you may need to set the event loop policy for async usage: |
| 226 | + |
| 227 | +```python |
| 228 | +import asyncio |
| 229 | +import sys |
| 230 | + |
| 231 | +if sys.platform == "win32": |
| 232 | + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) |
| 233 | +``` |
| 234 | + |
| 235 | +### Debug logging |
| 236 | + |
| 237 | +Enable debug logging to troubleshoot authentication issues: |
| 238 | + |
| 239 | +```python |
| 240 | +import logging |
| 241 | +logging.basicConfig(level=logging.DEBUG) |
| 242 | +``` |
| 243 | + |
| 244 | +## Next steps |
| 245 | + |
| 246 | +### Additional documentation |
| 247 | + |
| 248 | +For more information about Azure Database for PostgreSQL Entra ID authentication, see the |
| 249 | +[Azure documentation](https://learn.microsoft.com/azure/postgresql/security/security-entra-configure). |
| 250 | + |
| 251 | +### Samples |
| 252 | + |
| 253 | +Explore [sample code](https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/postgresql/azure-postgresql-auth/samples) for psycopg2, psycopg3, and SQLAlchemy. |
| 254 | + |
| 255 | +## Contributing |
| 256 | + |
| 257 | +This project welcomes contributions and suggestions. Most contributions require you to agree to a |
| 258 | +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us |
| 259 | +the rights to use your contribution. For details, visit [https://cla.microsoft.com](https://cla.microsoft.com). |
| 260 | + |
| 261 | +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide |
| 262 | +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions |
| 263 | +provided by the bot. You will only need to do this once across all repos using our CLA. |
| 264 | + |
| 265 | +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). |
| 266 | +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or |
| 267 | +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. |
| 268 | + |
| 269 | + |
0 commit comments