Skip to content

Commit 4832f63

Browse files
committed
Update azure-openai to check OIDC TokenCredential
1 parent 11c3638 commit 4832f63

File tree

17 files changed

+577
-6
lines changed

17 files changed

+577
-6
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
:project-version: 0.14.1
22
:langchain4j-version: 0.31.0
3-
:examples-dir: ./../examples/
3+
:examples-dir: ./../examples/

model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/runtime/AzureOpenAiRecorder.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ public Supplier<ChatLanguageModel> chatModel(LangChain4jAzureOpenAiConfig runtim
4545
String apiKey = azureAiConfig.apiKey().orElse(null);
4646
String adToken = azureAiConfig.adToken().orElse(null);
4747

48-
throwIfApiKeysNotConfigured(apiKey, adToken, configName);
48+
if (!runtimeConfig.useSecurityIdentityToken()) {
49+
throwIfApiKeysNotConfigured(apiKey, adToken, configName);
50+
}
4951

5052
var builder = AzureOpenAiChatModel.builder()
5153
.endpoint(getEndpoint(azureAiConfig, configName, EndpointType.CHAT))
@@ -91,7 +93,9 @@ public Supplier<StreamingChatLanguageModel> streamingChatModel(LangChain4jAzureO
9193
String apiKey = azureAiConfig.apiKey().orElse(null);
9294
String adToken = azureAiConfig.adToken().orElse(null);
9395

94-
throwIfApiKeysNotConfigured(apiKey, adToken, configName);
96+
if (!runtimeConfig.useSecurityIdentityToken()) {
97+
throwIfApiKeysNotConfigured(apiKey, adToken, configName);
98+
}
9599

96100
var builder = AzureOpenAiStreamingChatModel.builder()
97101
.endpoint(getEndpoint(azureAiConfig, configName, EndpointType.CHAT))
@@ -134,8 +138,10 @@ public Supplier<EmbeddingModel> embeddingModel(LangChain4jAzureOpenAiConfig runt
134138
EmbeddingModelConfig embeddingModelConfig = azureAiConfig.embeddingModel();
135139
String apiKey = azureAiConfig.apiKey().orElse(null);
136140
String adToken = azureAiConfig.adToken().orElse(null);
137-
if (apiKey == null && adToken == null) {
138-
throw new ConfigValidationException(createKeyMisconfigurationProblem(configName));
141+
if (!runtimeConfig.useSecurityIdentityToken()) {
142+
if (apiKey == null && adToken == null) {
143+
throw new ConfigValidationException(createKeyMisconfigurationProblem(configName));
144+
}
139145
}
140146
var builder = AzureOpenAiEmbeddingModel.builder()
141147
.endpoint(getEndpoint(azureAiConfig, configName, EndpointType.EMBEDDING))
@@ -169,7 +175,9 @@ public Supplier<ImageModel> imageModel(LangChain4jAzureOpenAiConfig runtimeConfi
169175
if (azureAiConfig.enableIntegration()) {
170176
var apiKey = azureAiConfig.apiKey().orElse(null);
171177
String adToken = azureAiConfig.adToken().orElse(null);
172-
throwIfApiKeysNotConfigured(apiKey, adToken, configName);
178+
if (!runtimeConfig.useSecurityIdentityToken()) {
179+
throwIfApiKeysNotConfigured(apiKey, adToken, configName);
180+
}
173181

174182
var imageModelConfig = azureAiConfig.imageModel();
175183
var builder = AzureOpenAiImageModel.builder()

model-providers/openai/azure-openai/runtime/src/main/java/io/quarkiverse/langchain4j/azure/openai/runtime/config/LangChain4jAzureOpenAiConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,13 @@ enum EndpointType {
187187
IMAGE
188188
}
189189
}
190+
191+
/**
192+
* Whether to use the current security identity's access token to access Azure OpenAI provider.
193+
* If it is set to {@code true} but the security identity has no access token then either OpenAI key or pre-configured Azure
194+
* token must be used.
195+
* Set to {@code false} to access Azure OpenAI provider only with the OpenAI key or pre-configured Azure token.
196+
*/
197+
@WithDefault("false")
198+
Boolean useSecurityIdentityToken();
190199
}

model-providers/openai/azure-openai/runtime/src/test/java/io/quarkiverse/langchain4j/azure/openai/runtime/AzureOpenAiRecorderEndpointTests.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,11 @@ public AzureAiConfig defaultConfig() {
126126
public Map<String, AzureAiConfig> namedConfig() {
127127
throw new IllegalStateException("should not be called");
128128
}
129+
130+
@Override
131+
public Boolean useSecurityIdentityToken() {
132+
return Boolean.TRUE;
133+
}
129134
}
130135

131136
static class CustomAzureAiConfig implements LangChain4jAzureOpenAiConfig.AzureAiConfig {

model-providers/openai/openai-common/runtime/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
<groupId>io.quarkus</groupId>
1818
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
1919
</dependency>
20+
<dependency>
21+
<groupId>io.quarkus.security</groupId>
22+
<artifactId>quarkus-security</artifactId>
23+
</dependency>
2024
<dependency>
2125
<groupId>io.quarkus</groupId>
2226
<artifactId>quarkus-qute</artifactId>

model-providers/openai/openai-common/runtime/src/main/java/io/quarkiverse/langchain4j/openai/OpenAiRestApi.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.util.function.Predicate;
1414

1515
import jakarta.annotation.Priority;
16+
import jakarta.enterprise.inject.Instance;
17+
import jakarta.inject.Inject;
1618
import jakarta.ws.rs.BeanParam;
1719
import jakarta.ws.rs.Consumes;
1820
import jakarta.ws.rs.HeaderParam;
@@ -38,6 +40,8 @@
3840
import org.jboss.resteasy.reactive.client.SseEvent;
3941
import org.jboss.resteasy.reactive.client.SseEventFilter;
4042
import org.jboss.resteasy.reactive.client.api.ClientLogger;
43+
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext;
44+
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter;
4145
import org.jboss.resteasy.reactive.common.providers.serialisers.AbstractJsonMessageBodyReader;
4246

4347
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -56,6 +60,7 @@
5660
import dev.ai4j.openai4j.moderation.ModerationResponse;
5761
import io.quarkiverse.langchain4j.QuarkusJsonCodecFactory;
5862
import io.quarkus.rest.client.reactive.ClientExceptionMapper;
63+
import io.quarkus.security.credential.TokenCredential;
5964
import io.smallrye.mutiny.Multi;
6065
import io.smallrye.mutiny.Uni;
6166
import io.vertx.core.Handler;
@@ -76,6 +81,7 @@
7681
@RegisterProvider(OpenAiRestApi.OpenAiRestApiJacksonWriter.class)
7782
@RegisterProvider(OpenAiRestApi.OpenAiRestApiReaderInterceptor.class)
7883
@RegisterProvider(OpenAiRestApi.OpenAiRestApiWriterInterceptor.class)
84+
@RegisterProvider(OpenAiRestApi.TokenCredentialFilter.class)
7985
public interface OpenAiRestApi {
8086

8187
/**
@@ -292,6 +298,19 @@ public void aroundWriteTo(WriterInterceptorContext context) throws IOException,
292298
}
293299
}
294300

301+
class TokenCredentialFilter implements ResteasyReactiveClientRequestFilter {
302+
303+
@Inject
304+
Instance<TokenCredential> tokenCredential;
305+
306+
@Override
307+
public void filter(ResteasyReactiveClientRequestContext context) {
308+
if (tokenCredential.isResolvable() && tokenCredential.get() != null) {
309+
context.getHeaders().add("Authorization", "Bearer " + tokenCredential.get().getToken());
310+
}
311+
}
312+
}
313+
295314
/**
296315
* Introduce a custom logger as the stock one logs at the DEBUG level by default...
297316
*/

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@
202202
<module>samples/chatbot</module>
203203
<module>samples/chatbot-easy-rag</module>
204204
<module>samples/sql-chatbot</module>
205+
<module>samples/secure-azure-openai-poem</module>
205206
</modules>
206207
</profile>
207208
</profiles>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Secure Vertex AI Gemini Poem Demo
2+
3+
This demo showcases the implementation of a secure Azure OpenAI Poem Demo which is available only to users authenticated with Azure.
4+
5+
## The Demo
6+
7+
### Setup
8+
9+
The demo asks Azure OpenAI LLM to write a short 1 paragraph poem, using the access token acquired during the OIDC authorization code flow.
10+
11+
### AI Service
12+
13+
This demo leverages the AI service abstraction, with the interaction between the LLM and the application handled through the AIService interface.
14+
15+
The `io.quarkiverse.langchain4j.sample.PoemAiService` interface uses specific annotations to define the LLM:
16+
17+
```java
18+
package io.quarkiverse.langchain4j.sample;
19+
20+
import dev.langchain4j.service.SystemMessage;
21+
import dev.langchain4j.service.UserMessage;
22+
import io.quarkiverse.langchain4j.RegisterAiService;
23+
24+
@RegisterAiService
25+
public interface PoemAiService {
26+
27+
/**
28+
* Ask the LLM to create a poem about Enterprise Java.
29+
*
30+
* @return the poem
31+
*/
32+
@SystemMessage("You are a professional poet")
33+
@UserMessage("""
34+
Write a short 1 paragraph poem about Java. Set an author name to the model name which created the poem.
35+
""")
36+
String writeAPoem();
37+
38+
}
39+
40+
### Using the AI service
41+
42+
Once defined, you can inject the AI service as a regular bean, and use it:
43+
44+
```java
45+
package io.quarkiverse.langchain4j.sample;
46+
47+
import java.net.URISyntaxException;
48+
49+
import io.quarkus.security.Authenticated;
50+
import jakarta.ws.rs.GET;
51+
import jakarta.ws.rs.Path;
52+
53+
@Path("/poem")
54+
@Authenticated
55+
public class PoemResource {
56+
57+
private final PoemAiService aiService;
58+
59+
public PoemResource(PoemAiService aiService) throws URISyntaxException {
60+
this.aiService = aiService;
61+
}
62+
63+
@GET
64+
public String getPoem() {
65+
return aiService.writeAPoem();
66+
}
67+
}
68+
69+
```
70+
71+
`PoemResource` can only be accessed by authenticated users.
72+
73+
## Azure Authentication
74+
75+
This demo requires users to authenticate with Azure OpenAI.
76+
All you need to do is to register an application with Azure, follow steps listed in the [Quarkus Microsoft](https://quarkus.io/guides/security-openid-connect-providers#microsoft) section, please keep in mind Azure Active Directory is now called Microsoft Entra ID.
77+
Name your application as `Quarkus LangChain4j AI`, and make sure an allowed callback URL is set to `http://localhost:8080/login`.
78+
Entra ID will generate a client id and secret, use them to set `quarkus.oidc.client-id` and `quarkus.oidc.credentials.secret` properties:
79+
80+
```properties
81+
quarkus.oidc.auth-server-url=https://login.microsoftonline.com/${AZURE_TENANT_ID}
82+
quarkus.oidc.application-type=web-app
83+
quarkus.oidc.token-state-manager.split-tokens=true
84+
quarkus.oidc.client-id=${AZURE_CLIENT_ID}
85+
quarkus.oidc.credentials.secret=${AZURE_CLIENT_SECRET}
86+
quarkus.oidc.authentication.extra-params.scope=${AZURE_OPENAI_SCOPES}
87+
quarkus.oidc.authentication.redirect-path=/login
88+
89+
quarkus.langchain4j.azure-openai.resource-name=${AZURE_OPENAI_RESOURCE}
90+
quarkus.langchain4j.azure-openai.deployment-name=${AZURE_OPENAI_DEPLOYMENT}
91+
quarkus.langchain4j.azure-openai.log-requests=true
92+
quarkus.langchain4j.azure-openai.log-responses=true
93+
quarkus.langchain4j.azure-openai.use-security-identity-token=true
94+
```
95+
96+
You must enable Azure OpenAI in your Azure tenant.
97+
98+
## Security Considerations
99+
100+
This demo makes it possible to access Azure OpenAI API enabled in the Azure tenant dashboard only to users who:
101+
102+
* Authenticated to Quarkus REST PoemService with Microsoft Entra ID using OIDC authorization code flow.
103+
* Authorized `Quarkus LangChain4j AI` application registered in Microsoft Entra ID to use the access token to access Azure OpenAI on behalf of the currently authentiicated user. This authorization is requested from users during the authentication process and is configured by adding `quarkus.oidc.authentication.extra-params.scope` in the application properties.
104+
* Quarkus LangChain4j azure-openai model provider uses this authorized token on behalf of the current user to access Azure OpenAI endpoint.
105+
106+
## Running the Demo
107+
108+
To run the demo, use the following commands:
109+
110+
```shell
111+
mvn quarkus:dev
112+
```
113+
114+
Then, access `http://localhost:8080`, login to Microsoft Entra ID, and follow a provided application link to read the poem.
115+
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<groupId>io.quarkiverse.langchain4j</groupId>
6+
<artifactId>quarkus-langchain4j-sample-secure-azure-openai-poem</artifactId>
7+
<name>Quarkus LangChain4j - Sample - Secure Azure OpenAI Poem</name>
8+
<version>1.0-SNAPSHOT</version>
9+
10+
<properties>
11+
<compiler-plugin.version>3.13.0</compiler-plugin.version>
12+
<maven.compiler.parameters>true</maven.compiler.parameters>
13+
<maven.compiler.release>17</maven.compiler.release>
14+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
15+
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
16+
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
17+
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
18+
<quarkus.platform.version>3.9.4</quarkus.platform.version>
19+
<skipITs>true</skipITs>
20+
<surefire-plugin.version>3.2.5</surefire-plugin.version>
21+
<quarkus-langchain4j.version>999-SNAPSHOT</quarkus-langchain4j.version>
22+
</properties>
23+
24+
<dependencyManagement>
25+
<dependencies>
26+
<dependency>
27+
<groupId>${quarkus.platform.group-id}</groupId>
28+
<artifactId>${quarkus.platform.artifact-id}</artifactId>
29+
<version>${quarkus.platform.version}</version>
30+
<type>pom</type>
31+
<scope>import</scope>
32+
</dependency>
33+
</dependencies>
34+
</dependencyManagement>
35+
36+
<dependencies>
37+
<dependency>
38+
<groupId>io.quarkus</groupId>
39+
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
40+
</dependency>
41+
<dependency>
42+
<groupId>io.quarkus</groupId>
43+
<artifactId>quarkus-oidc</artifactId>
44+
</dependency>
45+
<dependency>
46+
<groupId>io.quarkiverse.langchain4j</groupId>
47+
<artifactId>quarkus-langchain4j-azure-openai</artifactId>
48+
<version>${quarkus-langchain4j.version}</version>
49+
</dependency>
50+
<dependency>
51+
<groupId>io.quarkus</groupId>
52+
<artifactId>quarkus-resteasy-reactive-qute</artifactId>
53+
</dependency>
54+
</dependencies>
55+
<build>
56+
<plugins>
57+
<plugin>
58+
<groupId>io.quarkus</groupId>
59+
<artifactId>quarkus-maven-plugin</artifactId>
60+
<version>${quarkus.platform.version}</version>
61+
<executions>
62+
<execution>
63+
<goals>
64+
<goal>build</goal>
65+
</goals>
66+
</execution>
67+
</executions>
68+
</plugin>
69+
<plugin>
70+
<artifactId>maven-compiler-plugin</artifactId>
71+
<version>${compiler-plugin.version}</version>
72+
</plugin>
73+
<plugin>
74+
<artifactId>maven-surefire-plugin</artifactId>
75+
<version>3.2.5</version>
76+
<configuration>
77+
<systemPropertyVariables>
78+
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
79+
<maven.home>${maven.home}</maven.home>
80+
</systemPropertyVariables>
81+
</configuration>
82+
</plugin>
83+
</plugins>
84+
</build>
85+
86+
<profiles>
87+
<profile>
88+
<id>native</id>
89+
<activation>
90+
<property>
91+
<name>native</name>
92+
</property>
93+
</activation>
94+
<build>
95+
<plugins>
96+
<plugin>
97+
<artifactId>maven-failsafe-plugin</artifactId>
98+
<version>3.2.5</version>
99+
<executions>
100+
<execution>
101+
<goals>
102+
<goal>integration-test</goal>
103+
<goal>verify</goal>
104+
</goals>
105+
<configuration>
106+
<systemPropertyVariables>
107+
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
108+
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
109+
<maven.home>${maven.home}</maven.home>
110+
</systemPropertyVariables>
111+
</configuration>
112+
</execution>
113+
</executions>
114+
</plugin>
115+
</plugins>
116+
</build>
117+
<properties>
118+
<quarkus.package.type>native</quarkus.package.type>
119+
</properties>
120+
</profile>
121+
</profiles>
122+
</project>

0 commit comments

Comments
 (0)