Skip to content

Commit 35a91a8

Browse files
committed
Update vertex-ai-gemini to check OIDC TokenCredential
1 parent 11c3638 commit 35a91a8

File tree

15 files changed

+529
-3
lines changed

15 files changed

+529
-3
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/vertex-ai-gemini/runtime/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
2424
</dependency>
2525

26+
<dependency>
27+
<groupId>io.quarkus.security</groupId>
28+
<artifactId>quarkus-security</artifactId>
29+
</dependency>
30+
2631
<dependency>
2732
<groupId>com.google.auth</groupId>
2833
<artifactId>google-auth-library-oauth2-http</artifactId>

model-providers/vertex-ai-gemini/runtime/src/main/java/io/quarkiverse/langchain4j/vertexai/runtime/gemini/VertxAiGeminiRestApi.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.util.regex.Pattern;
1111

1212
import jakarta.enterprise.context.ApplicationScoped;
13+
import jakarta.enterprise.inject.Instance;
14+
import jakarta.inject.Inject;
1315
import jakarta.ws.rs.BeanParam;
1416
import jakarta.ws.rs.POST;
1517
import jakarta.ws.rs.Path;
@@ -27,6 +29,7 @@
2729

2830
import io.quarkus.arc.DefaultBean;
2931
import io.quarkus.rest.client.reactive.jackson.ClientObjectMapper;
32+
import io.quarkus.security.credential.TokenCredential;
3033
import io.vertx.core.Handler;
3134
import io.vertx.core.MultiMap;
3235
import io.vertx.core.buffer.Buffer;
@@ -128,6 +131,9 @@ class TokenFilter implements ResteasyReactiveClientRequestFilter {
128131
private final ExecutorService executorService;
129132
private final AuthProvider authProvider;
130133

134+
@Inject
135+
Instance<TokenCredential> tokenCredential;
136+
131137
public TokenFilter(ExecutorService executorService, AuthProvider authProvider) {
132138
this.executorService = executorService;
133139
this.authProvider = authProvider;
@@ -136,11 +142,13 @@ public TokenFilter(ExecutorService executorService, AuthProvider authProvider) {
136142
@Override
137143
public void filter(ResteasyReactiveClientRequestContext context) {
138144
context.suspend();
145+
final String currentToken = tokenCredential.isResolvable() ? tokenCredential.get().getToken() : null;
139146
executorService.submit(new Runnable() {
140147
@Override
141148
public void run() {
142149
try {
143-
context.getHeaders().add("Authorization", "Bearer " + authProvider.getBearerToken());
150+
context.getHeaders().add("Authorization", "Bearer " +
151+
(currentToken != null ? currentToken : authProvider.getBearerToken()));
144152
context.resume();
145153
} catch (Exception e) {
146154
context.resume(e);

model-providers/vertex-ai-gemini/runtime/src/main/java/io/quarkiverse/langchain4j/vertexai/runtime/gemini/config/LangChain4jVertexAiGeminiConfig.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ interface VertexAiGeminiConfig {
6060
Optional<String> baseUrl();
6161

6262
/**
63-
* Whether to enable the integration. Defaults to {@code true}, which means requests are made to the Anthropic
63+
* Whether to enable the integration. Defaults to {@code true}, which means requests are made to the Vertex AI
6464
* provider.
6565
* Set to {@code false} to disable all requests.
6666
*/
@@ -86,4 +86,13 @@ interface VertexAiGeminiConfig {
8686
*/
8787
ChatModelConfig chatModel();
8888
}
89+
90+
/**
91+
* Whether to use the current security identity's access token to access Vertex AI provider.
92+
* If it is set to {@code true} but the security identity has no access token then default Google application credentials
93+
* which must be setup in your environment will be used.
94+
* Set to {@code false} to access Vertex AI provider only with the default Google application credentials.
95+
*/
96+
@WithDefault("false")
97+
Boolean useSecurityIdentityToken();
8998
}

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-vertex-ai-gemini-poem</module>
205206
</modules>
206207
</profile>
207208
</profiles>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Secure Vertex AI Gemini Poem Demo
2+
3+
This demo showcases the implementation of a secure Vertex AI Gemini Poem Demo which is available only to users authenticated with Google.
4+
5+
## The Demo
6+
7+
### Setup
8+
9+
The demo asks Vertex AI Gemini 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+
## Google Authentication
74+
75+
This demo requires users to authenticate with Google.
76+
All you need to do is to register an application with Google, follow steps listed in the [Quarkus Google](https://quarkus.io/guides/security-openid-connect-providers#google) section.
77+
Name your Google application as `Quarkus LangChain4j AI`, and make sure an allowed callback URL is set to `http://localhost:8080/login`.
78+
Google 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.provider=google
82+
quarkus.oidc.client-id=${GOOGLE_CLIENT_ID}
83+
quarkus.oidc.credentials.secret=${GOOGLE_CLIENT_SECRET}
84+
quarkus.oidc.authentication.extra-params.scope=https://www.googleapis.com/auth/generative-language.retriever,https://www.googleapis.com/auth/cloud-platform
85+
quarkus.oidc.authentication.redirect-path=/login
86+
87+
# See https://cloud.google.com/vertex-ai/docs/general/locations
88+
vertex-ai-region=europe-west2
89+
90+
quarkus.langchain4j.vertexai.gemini.location=https://${vertex-ai-region}-aiplatform.googleapis.com
91+
quarkus.langchain4j.vertexai.gemini.project-id=${GOOGLE_PROJECT_ID}
92+
```
93+
94+
You must enable Vertex AI API in your Google Cloud project.
95+
96+
## Security Considerations
97+
98+
This demo makes it possible to access Google Vertex AI API enabled in the Google Cloud project only to users who:
99+
100+
* Authenticated to Quarkus REST PoemService with Google using OIDC authorization code flow.
101+
* Authorized `Quarkus LangChain4j AI` application registered in the Google Cloud project to use the access token to access Google Generative API 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=https://www.googleapis.com/auth/generative-language.retriever,https://www.googleapis.com/auth/cloud-platform` in the application properties.
102+
* Quarkus LangChain4j vertex-ai-gemini model provider uses this authorized token on behalf of the current user to access Google Vertex AI endpoint.
103+
104+
## Running the Demo
105+
106+
To run the demo, use the following commands:
107+
108+
```shell
109+
mvn quarkus:dev
110+
```
111+
112+
Then, access `http://localhost:8080`, login to Google, and follow a provided application link to read the poem.
113+
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-vertex-ai-gemini-poem</artifactId>
7+
<name>Quarkus LangChain4j - Sample - Secure Vertex AI Gemini 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-vertex-ai-gemini</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>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.quarkiverse.langchain4j.sample;
2+
3+
import org.eclipse.microprofile.jwt.JsonWebToken;
4+
5+
import io.quarkus.oidc.IdToken;
6+
import io.quarkus.qute.Template;
7+
import io.quarkus.qute.TemplateInstance;
8+
import io.quarkus.security.Authenticated;
9+
import jakarta.inject.Inject;
10+
import jakarta.ws.rs.GET;
11+
import jakarta.ws.rs.Path;
12+
import jakarta.ws.rs.Produces;
13+
14+
/**
15+
* Login resource which returns a poem welcome page to the authenticated user
16+
*/
17+
@Path("/login")
18+
@Authenticated
19+
public class LoginResource {
20+
21+
@Inject
22+
@IdToken
23+
JsonWebToken idToken;
24+
25+
@Inject
26+
Template poem;
27+
28+
@GET
29+
@Produces("text/html")
30+
public TemplateInstance poem() {
31+
return poem.data("name", idToken.getName());
32+
}
33+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.quarkiverse.langchain4j.sample;
2+
3+
import io.quarkus.oidc.OidcSession;
4+
import io.quarkus.security.Authenticated;
5+
import jakarta.inject.Inject;
6+
import jakarta.ws.rs.GET;
7+
import jakarta.ws.rs.Path;
8+
import jakarta.ws.rs.core.Context;
9+
import jakarta.ws.rs.core.Response;
10+
import jakarta.ws.rs.core.UriInfo;
11+
12+
/**
13+
* Logout resource
14+
*/
15+
@Path("/logout")
16+
@Authenticated
17+
public class LogoutResource {
18+
19+
@Inject
20+
OidcSession session;
21+
22+
@GET
23+
public Response logout(@Context UriInfo uriInfo) {
24+
// remove the local session cookie
25+
session.logout().await().indefinitely();
26+
// redirect to the login page
27+
return Response.seeOther(uriInfo.getBaseUriBuilder().path("login").build()).build();
28+
}
29+
}

0 commit comments

Comments
 (0)