Skip to content

Commit

Permalink
Azure Service Principal for Postgres SQL authentication - (#1169) (#1170
Browse files Browse the repository at this point in the history
)

* Azure Service Principal for Postgres SQL authentication -  (#1169)

* custom-backend changes Update in settings.py

Part of OASIS Service Principal Authentication

* custom backend wrapper Create base.py

* Create __init__.py

init

* Create __init__.py

* Create readme.md

* Update readme.md

* Update readme.md

* Update readme.md

* azure-identity package imported to requirements-server.in

* Update requirements-server.txt

* Update postgres.docker-compose.yml

SPA tenant_id, client_id params introduced to compose file

* Update readme.md

* Update readme.md

* Update readme.md

* Update readme.md

* Update readme.md

* Updated Package Requirements: azure-identity

* pep

---------

Co-authored-by: Saseem75 <[email protected]>
Co-authored-by: awsbuild <[email protected]>
  • Loading branch information
3 people committed Feb 27, 2025
1 parent a97f4a5 commit 6150ec7
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 5 deletions.
1 change: 1 addition & 0 deletions requirements-server.in
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ redis
sqlalchemy
mysqlclient==2.1.1
whitenoise
azure-identity
22 changes: 20 additions & 2 deletions requirements-server.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ automat==24.8.1
# via twisted
azure-core==1.31.0
# via
# azure-identity
# azure-storage-blob
# django-storages
azure-identity==1.20.0
# via -r requirements-server.in
azure-storage-blob==12.23.1
# via django-storages
billiard==4.2.1
Expand Down Expand Up @@ -83,9 +86,12 @@ cramjam==2.9.0
cryptography==43.0.3
# via
# autobahn
# azure-identity
# azure-storage-blob
# josepy
# mozilla-django-oidc
# msal
# pyjwt
# pyopenssl
# service-identity
daphne==2.5.0
Expand Down Expand Up @@ -191,6 +197,12 @@ markupsafe==3.0.2
# via jinja2
mozilla-django-oidc==4.0.1
# via -r requirements-server.in
msal==1.31.1
# via
# azure-identity
# msal-extensions
msal-extensions==1.2.0
# via azure-identity
msgpack==0.6.2
# via channels-redis
mysqlclient==2.1.1
Expand Down Expand Up @@ -223,6 +235,8 @@ pandas==2.2.3
# ods-tools
pathlib2==2.3.7.post1
# via -r requirements-server.in
portalocker==2.10.1
# via msal-extensions
prompt-toolkit==3.0.48
# via click-repl
psycopg2-binary==2.9.10
Expand All @@ -237,8 +251,10 @@ pyasn1-modules==0.4.1
# via service-identity
pycparser==2.22
# via cffi
pyjwt==2.9.0
# via djangorestframework-simplejwt
pyjwt[crypto]==2.9.0
# via
# djangorestframework-simplejwt
# msal
pymysql==1.1.1
# via -r requirements-server.in
pyopenssl==24.2.1
Expand Down Expand Up @@ -273,6 +289,7 @@ requests==2.32.3
# azure-core
# coreapi
# mozilla-django-oidc
# msal
rpds-py==0.20.0
# via
# jsonschema
Expand All @@ -299,6 +316,7 @@ txaio==23.1.1
typing-extensions==4.12.2
# via
# azure-core
# azure-identity
# azure-storage-blob
# oasis-data-manager
# sqlalchemy
Expand Down
2 changes: 1 addition & 1 deletion requirements-worker.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ azure-core==1.31.0
# azure-storage-blob
azure-datalake-store==0.0.53
# via adlfs
azure-identity==1.19.0
azure-identity==1.20.0
# via adlfs
azure-storage-blob==12.23.1
# via
Expand Down
6 changes: 4 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ azure-core==1.31.0
# django-storages
azure-datalake-store==0.0.53
# via adlfs
azure-identity==1.19.0
# via adlfs
azure-identity==1.20.0
# via
# -r /home/runner/work/OasisPlatform/OasisPlatform/requirements-server.in
# adlfs
azure-storage-blob==12.23.1
# via
# -r /home/runner/work/OasisPlatform/OasisPlatform/requirements-worker.in
Expand Down
Empty file.
Empty file.
39 changes: 39 additions & 0 deletions src/server/oasisapi/custom_db_backend/base/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.db.backends.postgresql.base import DatabaseWrapper as PostgresDatabaseWrapper
from azure.identity import ClientSecretCredential
import psycopg2


class DatabaseWrapper(PostgresDatabaseWrapper):
def __init__(self, settings_dict, alias='default'):
super().__init__(settings_dict, alias)
self.settings_dict = settings_dict # Store settings for later use
self.credential = None # Initialize credential object

def get_azure_access_token(self):
"""
Fetches a fresh access token using Azure Service Principal credentials.
"""
settings = self.settings_dict
tenant_id = settings.get('TENANT_ID')
client_id = settings.get('CLIENT_ID')
client_secret = settings.get('CLIENT_SECRET')

if not all([tenant_id, client_id, client_secret]):
raise ValueError("Azure AD credentials are missing in DATABASES settings.")

# Initialize credential object only once
if self.credential is None:
self.credential = ClientSecretCredential(tenant_id, client_id, client_secret)

# Fetch a fresh token
token = self.credential.get_token("https://ossrdbms-aad.database.windows.net/.default")
return token.token

def get_new_connection(self, conn_params):
"""
Establish a new PostgreSQL connection with a fresh Azure AD token.
"""
conn_params["password"] = self.get_azure_access_token() # Get fresh token
conn_params["sslmode"] = "require" # Ensure SSL connection

return psycopg2.connect(**conn_params)
99 changes: 99 additions & 0 deletions src/server/oasisapi/custom_db_backend/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Azure Service Principal for PostgreSQL Authentication

#### Defining custom DB Engine for Azure Service Principal Authentication
#### Tree

```/var/www/oasis/
│── src/
│ ├── server/
│ │ ├── oasisapi/
│ │ │ ├── settings.py # Configures database backend
│ │ │ ├── custom_db_backend/
│ │ │ │ ├── __init__.py # existing
│ │ │ │ ├── readme.md # Defining the custom DB Engine
│ │ │ │ ├── base/
│ │ │ │ | ├── base.py # where we have the DatabaseWrapper
│ │ │ │ | ├── __init__.py # existing
```

## Overview
This document explains how to implement **Azure Service Principal Authentication** with **token refresh** for a **Django application** using **PostgreSQL** as the authentication backend. The goal is to authenticate applications securely while enabling token renewal for continuous access.

#### Key Components
1. **Azure Service Principal**: A non-human identity used by applications to authenticate and authorize access to Azure resources.
2. **PostgreSQL Database**: A database-driven authentication system that stores service principal credentials and permissions.
3. **Custom PostgreSQL Database Wrapper**: Extends Django’s default PostgreSQL database backend to authenticate using Azure AD tokens.

#### Architecture
1. **Service Principal Registration**: Store client ID, hashed secret, and permissions in PostgreSQL.
2. **Authentication Flow**:
- The application sends authentication requests with client ID and secret.
- The Django authentication wrapper validates credentials against the database.
- On success, an access token and a refresh token are issued.
3. **Token Refresh Flow**:
- The application sends a refresh request with a valid refresh token.
- The wrapper validates the refresh token and issues a new access token.
4. **Database Authentication via Azure AD**:
- The custom database wrapper fetches a fresh Azure AD token for database authentication.

#### Implementation Steps
##### 1. *_Custom settings.py_*

```python
# For Azure Service Principal Authentication with token rotation

DATABASES = {
'default': {
'ENGINE': DB_ENGINE,
'NAME': iniconf.settings.get('server', 'db_name'),
'USER': iniconf.settings.get('server', 'AZURE_SERVICE_PRINCIPAL_USER'),
'PASSWORD': '', # Database-Custom-backendWrapper.get_token
'HOST': iniconf.settings.get('server', 'db_host'),
'PORT': iniconf.settings.get('server', 'db_port'),
'TENANT_ID': iniconf.settings.get('server','AZURE_TENANT_ID', fallback=None),
'CLIENT_ID': iniconf.settings.get('server','AZURE_CLIENT_ID', fallback=None),
'CLIENT_SECRET': iniconf.settings.get('server','AZURE_CLIENT_SECRET', fallback=None),
}
}
```

##### 2. Custom Database Backend (base.py)
Create a custom database backend that authenticates using Azure AD.

```python
class DatabaseWrapper(PostgresDatabaseWrapper):
def __init__(self, settings_dict, alias='default'):
super().__init__(settings_dict, alias)
self.settings_dict = settings_dict # Store settings for later use
self.credential = None # Initialize credential object

def get_azure_access_token(self):
"""
Fetches a fresh access token using Azure Service Principal credentials.
"""
settings = self.settings_dict
tenant_id = settings.get('TENANT_ID')
client_id = settings.get('CLIENT_ID')
client_secret = settings.get('CLIENT_SECRET')

if not all([tenant_id, client_id, client_secret]):
raise ValueError("Azure AD credentials are missing in DATABASES settings.")

# Initialize credential object only once
if self.credential is None:
self.credential = ClientSecretCredential(tenant_id, client_id, client_secret)

# Fetch a fresh token
token = self.credential.get_token("https://ossrdbms-aad.database.windows.net/.default")
return token.token
```


### Security Best Practices followed
- **Short-Lived Access Tokens**: Implement tokens with a short expiration time and refresh capability.
- **Access Control**: Enforce fine-grained permissions stored in PostgreSQL.
- **Secure Database Connection**: Enforce SSL and use short-lived Azure AD tokens for authentication.

## Conclusion
This implementation provides a secure way to authenticate Azure service principals using a Django application with PostgreSQL while enabling token refresh for uninterrupted access. The custom database backend ensures secure connections using Azure AD authentication.

22 changes: 22 additions & 0 deletions src/server/oasisapi/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,29 @@
'NAME': os.path.join(BASE_DIR, iniconf.settings.get('server', 'db_name', fallback='db.sqlite3')),
}
}


elif DB_ENGINE == 'src.server.oasisapi.custom_db_backend.base':

# For Azure Service Principal Authentication with token rotation

DATABASES = {
'default': {
'ENGINE': DB_ENGINE,
'NAME': iniconf.settings.get('server', 'db_name'),
'USER': iniconf.settings.get('server', 'AZURE_SERVICE_PRINCIPAL_USER'),
'PASSWORD': '', # Database-Custom-backendWrapper.get_token
'HOST': iniconf.settings.get('server', 'db_host'),
'PORT': iniconf.settings.get('server', 'db_port'),
'TENANT_ID': iniconf.settings.get('server', 'AZURE_TENANT_ID', fallback=None),
'CLIENT_ID': iniconf.settings.get('server', 'AZURE_CLIENT_ID', fallback=None),
'CLIENT_SECRET': iniconf.settings.get('server', 'AZURE_CLIENT_SECRET', fallback=None),
}
}


else:

DATABASES = {
'default': {
'ENGINE': DB_ENGINE,
Expand Down

0 comments on commit 6150ec7

Please sign in to comment.