Skip to content
Closed
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
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/includes/attributes.adoc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
:project-version: 0.14.1
:langchain4j-version: 0.31.0
:examples-dir: ./../examples/
:examples-dir: ./../examples/
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ public Supplier<ChatLanguageModel> chatModel(LangChain4jAzureOpenAiConfig runtim
String apiKey = azureAiConfig.apiKey().orElse(null);
String adToken = azureAiConfig.adToken().orElse(null);

throwIfApiKeysNotConfigured(apiKey, adToken, configName);
if (!runtimeConfig.useSecurityIdentityToken()) {
throwIfApiKeysNotConfigured(apiKey, adToken, configName);
}

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

throwIfApiKeysNotConfigured(apiKey, adToken, configName);
if (!runtimeConfig.useSecurityIdentityToken()) {
throwIfApiKeysNotConfigured(apiKey, adToken, configName);
}

var builder = AzureOpenAiStreamingChatModel.builder()
.endpoint(getEndpoint(azureAiConfig, configName, EndpointType.CHAT))
Expand Down Expand Up @@ -134,8 +138,10 @@ public Supplier<EmbeddingModel> embeddingModel(LangChain4jAzureOpenAiConfig runt
EmbeddingModelConfig embeddingModelConfig = azureAiConfig.embeddingModel();
String apiKey = azureAiConfig.apiKey().orElse(null);
String adToken = azureAiConfig.adToken().orElse(null);
if (apiKey == null && adToken == null) {
throw new ConfigValidationException(createKeyMisconfigurationProblem(configName));
if (!runtimeConfig.useSecurityIdentityToken()) {
if (apiKey == null && adToken == null) {
throw new ConfigValidationException(createKeyMisconfigurationProblem(configName));
}
}
var builder = AzureOpenAiEmbeddingModel.builder()
.endpoint(getEndpoint(azureAiConfig, configName, EndpointType.EMBEDDING))
Expand Down Expand Up @@ -169,7 +175,9 @@ public Supplier<ImageModel> imageModel(LangChain4jAzureOpenAiConfig runtimeConfi
if (azureAiConfig.enableIntegration()) {
var apiKey = azureAiConfig.apiKey().orElse(null);
String adToken = azureAiConfig.adToken().orElse(null);
throwIfApiKeysNotConfigured(apiKey, adToken, configName);
if (!runtimeConfig.useSecurityIdentityToken()) {
throwIfApiKeysNotConfigured(apiKey, adToken, configName);
}

var imageModelConfig = azureAiConfig.imageModel();
var builder = AzureOpenAiImageModel.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,13 @@ enum EndpointType {
IMAGE
}
}

/**
* Whether to use the current security identity's access token to access Azure OpenAI provider.
* If it is set to {@code true} but the security identity has no access token then either OpenAI key or pre-configured Azure
* token must be used.
* Set to {@code false} to access Azure OpenAI provider only with the OpenAI key or pre-configured Azure token.
*/
@WithDefault("false")
Boolean useSecurityIdentityToken();
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ public AzureAiConfig defaultConfig() {
public Map<String, AzureAiConfig> namedConfig() {
throw new IllegalStateException("should not be called");
}

@Override
public Boolean useSecurityIdentityToken() {
return Boolean.TRUE;
}
}

static class CustomAzureAiConfig implements LangChain4jAzureOpenAiConfig.AzureAiConfig {
Expand Down
4 changes: 4 additions & 0 deletions model-providers/openai/openai-common/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus.security</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import java.util.function.Predicate;

import jakarta.annotation.Priority;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.HeaderParam;
Expand All @@ -38,6 +40,8 @@
import org.jboss.resteasy.reactive.client.SseEvent;
import org.jboss.resteasy.reactive.client.SseEventFilter;
import org.jboss.resteasy.reactive.client.api.ClientLogger;
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext;
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter;
import org.jboss.resteasy.reactive.common.providers.serialisers.AbstractJsonMessageBodyReader;

import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -56,6 +60,7 @@
import dev.ai4j.openai4j.moderation.ModerationResponse;
import io.quarkiverse.langchain4j.QuarkusJsonCodecFactory;
import io.quarkus.rest.client.reactive.ClientExceptionMapper;
import io.quarkus.security.credential.TokenCredential;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.vertx.core.Handler;
Expand All @@ -76,6 +81,7 @@
@RegisterProvider(OpenAiRestApi.OpenAiRestApiJacksonWriter.class)
@RegisterProvider(OpenAiRestApi.OpenAiRestApiReaderInterceptor.class)
@RegisterProvider(OpenAiRestApi.OpenAiRestApiWriterInterceptor.class)
@RegisterProvider(OpenAiRestApi.TokenCredentialFilter.class)
public interface OpenAiRestApi {

/**
Expand Down Expand Up @@ -292,6 +298,19 @@ public void aroundWriteTo(WriterInterceptorContext context) throws IOException,
}
}

class TokenCredentialFilter implements ResteasyReactiveClientRequestFilter {

@Inject
Instance<TokenCredential> tokenCredential;

@Override
public void filter(ResteasyReactiveClientRequestContext context) {
if (tokenCredential.isResolvable() && tokenCredential.get() != null) {
context.getHeaders().add("Authorization", "Bearer " + tokenCredential.get().getToken());
}
Comment on lines +308 to +310
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this take useSecurityIdentityToken into account?

}
}

/**
* Introduce a custom logger as the stock one logs at the DEBUG level by default...
*/
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
<module>samples/chatbot</module>
<module>samples/chatbot-easy-rag</module>
<module>samples/sql-chatbot</module>
<module>samples/secure-azure-openai-poem</module>
</modules>
</profile>
</profiles>
Expand Down
115 changes: 115 additions & 0 deletions samples/secure-azure-openai-poem/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Secure Vertex AI Gemini Poem Demo
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong title :)


This demo showcases the implementation of a secure Azure OpenAI Poem Demo which is available only to users authenticated with Azure.

## The Demo

### Setup

The demo asks Azure OpenAI LLM to write a short 1 paragraph poem, using the access token acquired during the OIDC authorization code flow.

### AI Service

This demo leverages the AI service abstraction, with the interaction between the LLM and the application handled through the AIService interface.

The `io.quarkiverse.langchain4j.sample.PoemAiService` interface uses specific annotations to define the LLM:

```java
package io.quarkiverse.langchain4j.sample;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;

@RegisterAiService
public interface PoemAiService {

/**
* Ask the LLM to create a poem about Enterprise Java.
*
* @return the poem
*/
@SystemMessage("You are a professional poet")
@UserMessage("""
Write a short 1 paragraph poem about Java. Set an author name to the model name which created the poem.
""")
String writeAPoem();

}

### Using the AI service

Once defined, you can inject the AI service as a regular bean, and use it:

```java
package io.quarkiverse.langchain4j.sample;

import java.net.URISyntaxException;

import io.quarkus.security.Authenticated;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/poem")
@Authenticated
public class PoemResource {

private final PoemAiService aiService;

public PoemResource(PoemAiService aiService) throws URISyntaxException {
this.aiService = aiService;
}

@GET
public String getPoem() {
return aiService.writeAPoem();
}
}

```

`PoemResource` can only be accessed by authenticated users.

## Azure Authentication

This demo requires users to authenticate with Azure OpenAI.
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.
Name your application as `Quarkus LangChain4j AI`, and make sure an allowed callback URL is set to `http://localhost:8080/login`.
Entra ID will generate a client id and secret, use them to set `quarkus.oidc.client-id` and `quarkus.oidc.credentials.secret` properties:

```properties
quarkus.oidc.auth-server-url=https://login.microsoftonline.com/${AZURE_TENANT_ID}
quarkus.oidc.application-type=web-app
quarkus.oidc.token-state-manager.split-tokens=true
quarkus.oidc.client-id=${AZURE_CLIENT_ID}
quarkus.oidc.credentials.secret=${AZURE_CLIENT_SECRET}
quarkus.oidc.authentication.extra-params.scope=${AZURE_OPENAI_SCOPES}
quarkus.oidc.authentication.redirect-path=/login

quarkus.langchain4j.azure-openai.resource-name=${AZURE_OPENAI_RESOURCE}
quarkus.langchain4j.azure-openai.deployment-name=${AZURE_OPENAI_DEPLOYMENT}
quarkus.langchain4j.azure-openai.log-requests=true
quarkus.langchain4j.azure-openai.log-responses=true
quarkus.langchain4j.azure-openai.use-security-identity-token=true
```

You must enable Azure OpenAI in your Azure tenant.

## Security Considerations

This demo makes it possible to access Azure OpenAI API enabled in the Azure tenant dashboard only to users who:

* Authenticated to Quarkus REST PoemService with Microsoft Entra ID using OIDC authorization code flow.
* 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.
* Quarkus LangChain4j azure-openai model provider uses this authorized token on behalf of the current user to access Azure OpenAI endpoint.

## Running the Demo

To run the demo, use the following commands:

```shell
mvn quarkus:dev
```

Then, access `http://localhost:8080`, login to Microsoft Entra ID, and follow a provided application link to read the poem.

122 changes: 122 additions & 0 deletions samples/secure-azure-openai-poem/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>

<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-sample-secure-azure-openai-poem</artifactId>
<name>Quarkus LangChain4j - Sample - Secure Azure OpenAI Poem</name>
<version>1.0-SNAPSHOT</version>

<properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.parameters>true</maven.compiler.parameters>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<quarkus.platform.version>3.9.4</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.2.5</surefire-plugin.version>
<quarkus-langchain4j.version>999-SNAPSHOT</quarkus-langchain4j.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-azure-openai</artifactId>
<version>${quarkus-langchain4j.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-qute</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.2.5</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>
</project>
Loading