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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 92 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ This repository provides sample implementations in Python, JavaScript, and .NET
## Table of Contents
- [Overview](#overview)
- [Python Usage](#python-usage)
- [Java Usage](#java-usage)
- [JavaScript Usage](#javascript-usage)
- [Dotnet Usage](#dotnet-usage)

## Overview
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.
To support this, we've created extension methods for Npgsql (for .NET), psycopg (for Python), and node-postgres/Sequelize (for JavaScript). 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.
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.

## Python Usage

Expand Down Expand Up @@ -102,6 +103,89 @@ pool = AsyncConnectionPool(
Use `python/sample.py` as a runnable demo that shows loading configuration from `.env` and creating the pool. If you copy `AsyncEntraConnection` into your own project you don't need the sample's `.env` or exact runtime layout — just supply host/DB settings however your application normally gets configuration.


## Java Usage

This repository provides Entra ID authentication samples for Java applications using PostgreSQL, with support for both plain JDBC and Hibernate ORM.

### Prerequisites
- Java 17 or higher
- Maven 3.6+ (for dependency management)
- Azure Identity Extensions library

### Setup

1. **Install Maven dependencies:**

The project includes a `pom.xml` file with all required dependencies. Navigate to the `java` folder and run:

```powershell
cd java
mvn clean compile

2. **Configure database connection:**

Create or edit `application.properties` in the `java` folder:

```properties
url=jdbc:postgresql://<your-server>.postgres.database.azure.com:5432/<database>?sslmode=require&authenticationPluginClassName=com.azure.identity.extensions.jdbc.postgresql.AzurePostgresqlAuthenticationPlugin
user=<your-username>@<your-domain>.onmicrosoft.com

### Running the Examples

The project includes two examples:
- `EntraIdExtensionJdbc.java` - Basic JDBC and HikariCP connection pooling
- `EntraIdExtensionHibernate.java` - Hibernate ORM with Entra ID authentication

**To switch between examples**, edit the `<mainClass>` property in `pom.xml`:

```xml
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<!-- Change this line to switch between examples -->
<mainClass>EntraIdExtensionJdbc</mainClass>
<!-- Or use: <mainClass>EntraIdExtensionHibernate</mainClass> -->
</configuration>
</plugin>
```

Then run:
```powershell
cd java
mvn exec:java
```

**Note:** Do not use VS Code's "Run" button directly. Run examples through Maven to ensure proper classpath and resource loading.

### Using in Your Own Project

To integrate Entra ID authentication into your own Java project, you can follow the same setup steps for running the examples.

### How Token Refresh Works (Java)

The Azure Identity Extensions library (`azure-identity-extensions`) automatically handles token refresh:

1. **Authentication Plugin**: The JDBC URL includes `authenticationPluginClassName=com.azure.identity.extensions.jdbc.postgresql.AzurePostgresqlAuthenticationPlugin` which intercepts connection attempts.

2. **Token Acquisition**: The plugin uses `DefaultAzureCredential` to automatically acquire Entra ID access tokens scoped for Azure Database for PostgreSQL.

3. **Automatic Refresh**:
- For single connections: A fresh token is acquired for each connection
- For connection pools: Tokens are refreshed automatically when connections are created or revalidated
- The `maxLifetime` setting in HikariCP (30 minutes) ensures connections are recycled before token expiration

4. **Credential Discovery**: DefaultAzureCredential attempts authentication in this order:
- Environment variables
- Managed Identity
- Azure CLI credentials
- IntelliJ credentials
- VS Code credentials
- And more...

This design ensures tokens are always valid without manual refresh logic, and connection pools automatically handle token lifecycle.

## JavaScript Usage

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.
Expand All @@ -120,7 +204,7 @@ This repository provides Entra ID authentication samples for JavaScript/Node.js
cd javascript
npm install
```

2. **Configure database connection:**

Create a `.env` file in the `javascript` folder:
Expand All @@ -134,6 +218,7 @@ This repository provides Entra ID authentication samples for JavaScript/Node.js

Replace:
- `<your-server>` with your Azure PostgreSQL server hostname
- `<database>` with your database name
- `<your-database>` with your database name
- `<your-username>@<your-domain>.onmicrosoft.com` with your Entra ID user principal name

Expand Down Expand Up @@ -234,11 +319,15 @@ The `entra-connection.js` module provides helper functions for Entra ID authenti
- Environment variables
- Managed Identity
- Azure CLI credentials
- IntelliJ credentials
- VS Code credentials
- And more...

```
This design ensures tokens are always valid without manual refresh logic, and connection pools automatically handle token lifecycle.
- VS Code credentials
- And more...

```

## Dotnet Usage

Expand Down
130 changes: 130 additions & 0 deletions java/EntraIdExtensionHibernate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import java.util.Properties;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class EntraIdExtensionHibernate {

private static SessionFactory sessionFactory;

public static void main(String[] args) {
try {
// Create SessionFactory
sessionFactory = createSessionFactory();

// Test the connection
testDatabaseConnection();

} catch (Exception e) {
System.err.println("Failed to create SessionFactory or connect to database:");
e.printStackTrace();
} finally {
// Clean up
if (sessionFactory != null) {
sessionFactory.close();
}
}
}

/**
* Create Hibernate SessionFactory with Azure AD authentication
*/
private static SessionFactory createSessionFactory() {
// Load configuration from application.properties
Properties appProps = loadApplicationProperties();

String url = appProps.getProperty("url");
String user = appProps.getProperty("user");

if (url == null || url.trim().isEmpty()) {
throw new RuntimeException("URL not found in application.properties");
}

if (user == null || user.trim().isEmpty()) {
throw new RuntimeException("User not found in application.properties");
}

// Configure Hibernate properties
Properties hibernateProps = new Properties();

// Database connection settings
hibernateProps.setProperty("hibernate.connection.driver_class", "org.postgresql.Driver");
hibernateProps.setProperty("hibernate.connection.url", url);
hibernateProps.setProperty("hibernate.connection.username", user);

// Hibernate settings
hibernateProps.setProperty(Environment.DIALECT, "org.hibernate.dialect.PostgreSQLDialect");
hibernateProps.setProperty(Environment.SHOW_SQL, "true");
hibernateProps.setProperty(Environment.FORMAT_SQL, "true");
hibernateProps.setProperty(Environment.HBM2DDL_AUTO, "none"); // Don't auto-create tables

// Connection pool settings (using Hibernate's built-in pool)
hibernateProps.setProperty(Environment.POOL_SIZE, "5");
hibernateProps.setProperty(Environment.AUTOCOMMIT, "true");

try {
Configuration configuration = new Configuration();
configuration.setProperties(hibernateProps);

System.out.println("Creating Hibernate SessionFactory...");
return configuration.buildSessionFactory();

} catch (Exception e) {
throw new RuntimeException("Failed to create SessionFactory", e);
}
}

/**
* Load properties from application.properties file
*/
private static Properties loadApplicationProperties() {
Properties props = new Properties();
try (InputStream input = new FileInputStream("application.properties")) {
if (input == null) {
throw new RuntimeException("Unable to find application.properties");
}
props.load(input);
return props;
} catch (IOException e) {
throw new RuntimeException("Error loading application.properties: " + e.getMessage(), e);
}
}

/**
* Test the database connection by executing a simple query
*/
private static void testDatabaseConnection() {
System.out.println("Testing database connection...");

try (Session session = sessionFactory.openSession()) {
// Execute a simple query to test the connection
String currentUser = session.createNativeQuery("SELECT current_user", String.class).getSingleResult();
String currentTime = session.createNativeQuery("SELECT NOW()", String.class).getSingleResult();
String version = session.createNativeQuery("SELECT version()", String.class).getSingleResult();

System.out.println("Successfully connected to PostgreSQL!");
System.out.println("Current user: " + currentUser);
System.out.println("Current time: " + currentTime);
System.out.println("PostgreSQL version: " + version.substring(0, Math.min(version.length(), 50)) + "...");

// Test multiple sessions to verify connection pooling
System.out.println("\nTesting connection pooling...");
for (int i = 1; i <= 3; i++) {
try (Session testSession = sessionFactory.openSession()) {
String result = testSession.createNativeQuery("SELECT 'Test query #" + i + "'", String.class)
.getSingleResult();
System.out.println(" " + result + " - Session created successfully");
}
}
System.out.println("Connection pooling is working!");

} catch (Exception e) {
System.err.println("Database connection test failed:");
e.printStackTrace();
}
}
}
129 changes: 129 additions & 0 deletions java/EntraIdExtensionJdbc.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import java.sql.DriverManager;
import java.util.Properties;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class EntraIdExtensionJdbc {
public static void main(String[] args) {
Properties config = loadApplicationProperties();

// Get URL and user from properties
String url = config.getProperty("url");
String user = config.getProperty("user");

if (url == null || url.trim().isEmpty()) {
System.err.println("URL not found in application.properties");
return;
}

if (user == null || user.trim().isEmpty()) {
System.err.println("User not found in application.properties");
return;
}

// Demonstrate basic JDBC connection
demonstrateBasicJdbc(url, user);

System.out.println("\n" + "=".repeat(60) + "\n");

// Demonstrate connection pooling
demonstrateConnectionPooling(url, user);
}

/**
* Demonstrate basic JDBC connection using DriverManager and Azure
* authentication plugin
*/
private static void demonstrateBasicJdbc(String url, String user) {
System.out.println("Basic JDBC Connection (no pooling):");

// Create connection properties
Properties props = new Properties();
props.setProperty("user", user);

try (Connection conn = DriverManager.getConnection(url, props)) {
System.out.println("Connected successfully using automatic token retrieval!");
var rs = conn.createStatement().executeQuery("SELECT current_user;");
if (rs.next()) {
System.out.println("Current database user: " + rs.getString(1));
}
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* Demonstrate connection pooling with HikariCP using Azure authentication
* plugin
*/
private static void demonstrateConnectionPooling(String jdbcUrl, String user) {
System.out.println("Connection Pooling with HikariCP:");

// Configure HikariCP with JDBC URL (the Azure plugin handles authentication)
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcUrl);
config.setUsername(user);

// Pool configuration
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30000); // 30 seconds
config.setIdleTimeout(600000); // 10 minutes
config.setMaxLifetime(1800000); // 30 minutes (less than token lifetime)
config.setPoolName("PostgreSQL-Azure-Pool");

try (HikariDataSource pooledDataSource = new HikariDataSource(config)) {
System.out.println("Connection pool created with " + config.getMaximumPoolSize() + " max connections");
// Execute multiple queries using the pool
for (int i = 1; i <= 3; i++) {
try (Connection conn = pooledDataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT 'Query #" + i + "' AS query_num, NOW() AS time, current_user AS user")) {

if (rs.next()) {
System.out.println(" " + rs.getString("query_num") + " - " + rs.getTimestamp("time")
+ " - User: " + rs.getString("user"));
}

} catch (Exception e) {
System.err.println("Query " + i + " failed:");
e.printStackTrace();
}
}

System.out.println("All pooled queries completed successfully");
System.out.println("Pool stats - Active: " + pooledDataSource.getHikariPoolMXBean().getActiveConnections()
+ ", Idle: " + pooledDataSource.getHikariPoolMXBean().getIdleConnections() + ", Total: "
+ pooledDataSource.getHikariPoolMXBean().getTotalConnections());

} catch (Exception e) {
System.err.println("Connection pooling failed:");
e.printStackTrace();
}
}

private static Properties loadApplicationProperties() {
Properties config = new Properties();
try (InputStream input = EntraIdExtensionJdbc.class.getClassLoader()
.getResourceAsStream("application.properties")) {
if (input == null) {
System.err.println("Unable to find application.properties");
System.exit(1);
}
config.load(input);
return config;
} catch (IOException e) {
System.err.println("Error loading application.properties: " + e.getMessage());
e.printStackTrace();
System.exit(1);
return config;
}
}
}
Loading