@@ -101,7 +101,10 @@ public static IServiceCollection AddAzureOpenAIServices(
101101
102102 /// <summary>
103103 /// Adds PostgreSQL vector store with managed identity authentication support.
104- /// Uses periodic token refresh to ensure tokens are renewed before expiry.
104+ /// Uses per-connection token refresh via <c>UsePasswordProvider</c>, which calls
105+ /// <see cref="TokenCredential.GetTokenAsync"/> on every new physical connection.
106+ /// <see cref="DefaultAzureCredential"/> caches tokens internally and auto-refreshes
107+ /// ~5 minutes before expiry, so this does not add Azure AD overhead.
105108 /// </summary>
106109 /// <param name="services">The service collection to add services to</param>
107110 /// <param name="connectionString">The PostgreSQL connection string (without password)</param>
@@ -133,18 +136,30 @@ private static IServiceCollection AddPostgresVectorStoreWithManagedIdentity(
133136 dataSourceBuilder . ConnectionStringBuilder . SslMode = SslMode . Require ;
134137 }
135138
136- // Use periodic token refresh instead of a one-shot token at startup.
137- // Azure AD tokens expire after ~1 hour; refreshing every 50 minutes
138- // ensures uninterrupted connectivity for long-running applications.
139- dataSourceBuilder . UsePeriodicPasswordProvider (
140- async ( _ , ct ) =>
141- {
142- var tokenRequestContext = new TokenRequestContext ( _PostgresScopes ) ;
143- var accessToken = await credential . GetTokenAsync ( tokenRequestContext , ct ) ;
144- return accessToken . Token ;
145- } ,
146- TimeSpan . FromMinutes ( 50 ) ,
147- TimeSpan . FromSeconds ( 10 ) ) ;
139+ var tokenRequestContext = new TokenRequestContext ( _PostgresScopes ) ;
140+
141+ // UsePasswordProvider is called for every new physical connection.
142+ // DefaultAzureCredential caches tokens internally and auto-refreshes ~5 min before
143+ // expiry — no extra Azure AD load. This is the approach recommended by the Npgsql
144+ // docs for cloud providers that implement their own caching (Azure MI does).
145+ // UsePeriodicPasswordProvider is only for token sources without built-in caching.
146+ // See: https://www.npgsql.org/doc/security.html
147+ // See: https://github.com/npgsql/npgsql/issues/5186
148+ //
149+ // Note: The username is expected to be set in the connection string already
150+ // (Aspire sets it during deployment for Azure PostgreSQL Flexible Server).
151+ // If a standalone username-extraction fallback is ever needed, use the
152+ // Microsoft.Azure.PostgreSQL.Auth package (UseEntraAuthentication extension)
153+ // once it ships on NuGet.
154+ dataSourceBuilder . UsePasswordProvider (
155+ passwordProvider : _ => credential . GetToken ( tokenRequestContext , default ) . Token ,
156+ passwordProviderAsync : async ( _ , ct ) =>
157+ ( await credential . GetTokenAsync ( tokenRequestContext , ct ) ) . Token ) ;
158+
159+ // Recycle pooled connections after 50 min, well before the 60-min JWT token TTL.
160+ // Combined with UsePasswordProvider (called on every new physical connection),
161+ // this ensures no pooled connection ever holds an expired token.
162+ dataSourceBuilder . ConnectionStringBuilder . ConnectionLifetime = 3000 ;
148163 }
149164
150165 return dataSourceBuilder . Build ( ) ;
0 commit comments