diff --git a/extensions/oidc/deployment/pom.xml b/extensions/oidc/deployment/pom.xml
index 9e30fa1a60198..2aa0c2fa9a4a1 100644
--- a/extensions/oidc/deployment/pom.xml
+++ b/extensions/oidc/deployment/pom.xml
@@ -102,7 +102,12 @@
maven-surefire-plugin
- true
+
+
+
+
+ ServicePublicKeyTestCase.java
+
@@ -121,6 +126,9 @@
maven-surefire-plugin
false
+
+ ServicePublicKeyTestCase.java
+
${keycloak.url}
diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/KeycloakDevModeRealmResourceManager.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/KeycloakDevModeRealmResourceManager.java
index de09e7640f5a0..c11e58f7b4bfb 100644
--- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/KeycloakDevModeRealmResourceManager.java
+++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/KeycloakDevModeRealmResourceManager.java
@@ -26,24 +26,25 @@ public class KeycloakDevModeRealmResourceManager implements QuarkusTestResourceL
@Override
public Map start() {
-
- try {
-
- RealmRepresentation realm = createRealm(KEYCLOAK_REALM);
-
- realm.getClients().add(createClient("client-dev-mode"));
- realm.getUsers().add(createUser("alice-dev-mode", "user"));
-
- RestAssured
- .given()
- .auth().oauth2(getAdminAccessToken())
- .contentType("application/json")
- .body(JsonSerialization.writeValueAsBytes(realm))
- .when()
- .post(KEYCLOAK_SERVER_URL + "/admin/realms").then()
- .statusCode(201);
- } catch (IOException e) {
- throw new RuntimeException(e);
+ if (System.getProperty("keycloak.not.required") == null) {
+ try {
+
+ RealmRepresentation realm = createRealm(KEYCLOAK_REALM);
+
+ realm.getClients().add(createClient("client-dev-mode"));
+ realm.getUsers().add(createUser("alice-dev-mode", "user"));
+
+ RestAssured
+ .given()
+ .auth().oauth2(getAdminAccessToken())
+ .contentType("application/json")
+ .body(JsonSerialization.writeValueAsBytes(realm))
+ .when()
+ .post(KEYCLOAK_SERVER_URL + "/admin/realms").then()
+ .statusCode(201);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
return Collections.emptyMap();
}
@@ -113,11 +114,12 @@ private static UserRepresentation createUser(String username, String... realmRol
@Override
public void stop() {
-
- RestAssured
- .given()
- .auth().oauth2(getAdminAccessToken())
- .when()
- .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).thenReturn().prettyPrint();
+ if (System.getProperty("keycloak.not.required") == null) {
+ RestAssured
+ .given()
+ .auth().oauth2(getAdminAccessToken())
+ .when()
+ .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).thenReturn().prettyPrint();
+ }
}
}
diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ServiceProtectedResource.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ServiceProtectedResource.java
new file mode 100644
index 0000000000000..815973fe7bd9a
--- /dev/null
+++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ServiceProtectedResource.java
@@ -0,0 +1,22 @@
+package io.quarkus.oidc.test;
+
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+
+import org.eclipse.microprofile.jwt.JsonWebToken;
+
+import io.quarkus.security.Authenticated;
+
+@Path("/service")
+@Authenticated
+public class ServiceProtectedResource {
+
+ @Inject
+ JsonWebToken accessToken;
+
+ @GET
+ public String getName() {
+ return accessToken.getName();
+ }
+}
diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ServicePublicKeyTestCase.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ServicePublicKeyTestCase.java
new file mode 100644
index 0000000000000..084e9c7c0c477
--- /dev/null
+++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ServicePublicKeyTestCase.java
@@ -0,0 +1,46 @@
+package io.quarkus.oidc.test;
+
+import java.io.IOException;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+import io.restassured.response.Response;
+import io.smallrye.jwt.build.Jwt;
+
+public class ServicePublicKeyTestCase {
+
+ private static Class>[] testClasses = {
+ ServiceProtectedResource.class
+ };
+
+ @RegisterExtension
+ static final QuarkusUnitTest test = new QuarkusUnitTest()
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
+ .addClasses(testClasses)
+ .addAsResource("privateKey.pem")
+ .addAsResource("application-service-public-key.properties", "application.properties"));
+
+ @Test
+ public void testAccessTokenInjection() throws IOException, InterruptedException {
+ String jwt = Jwt.claims().preferredUserName("alice").sign();
+ Assertions.assertEquals("alice", RestAssured.given().auth()
+ .oauth2(jwt)
+ .get("/service").getBody().asString());
+ }
+
+ @Test
+ public void testModifiedSignature() throws IOException, InterruptedException {
+ String jwt = Jwt.claims().preferredUserName("alice").sign();
+ // the last section of the jwt token is a signature
+ Response r = RestAssured.given().auth()
+ .oauth2(jwt + "1")
+ .get("/service");
+ Assertions.assertEquals(403, r.getStatusCode());
+ }
+}
diff --git a/extensions/oidc/deployment/src/test/resources/application-service-public-key.properties b/extensions/oidc/deployment/src/test/resources/application-service-public-key.properties
new file mode 100644
index 0000000000000..58a1d19270db4
--- /dev/null
+++ b/extensions/oidc/deployment/src/test/resources/application-service-public-key.properties
@@ -0,0 +1,3 @@
+quarkus.oidc.client-id=test
+quarkus.oidc.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB
+smallrye.jwt.sign.key-location=/privateKey.pem
\ No newline at end of file
diff --git a/extensions/oidc/deployment/src/test/resources/privateKey.pem b/extensions/oidc/deployment/src/test/resources/privateKey.pem
new file mode 100644
index 0000000000000..27543a434a1eb
--- /dev/null
+++ b/extensions/oidc/deployment/src/test/resources/privateKey.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa
+PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H
+OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN
+qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh
+nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM
+uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6
+oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv
+6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY
+URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6
+96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB
+Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3
+zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF
+KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP
+iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B
+m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS
+34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG
+5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2
+tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL
+WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y
+b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09
+nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB
+MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d
+Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe
+Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt
+FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8
+f3cg+fr8aou7pr9SHhJlZCU=
+-----END PRIVATE KEY-----
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java
index 837fd50edeb64..5a088f5ac7689 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java
@@ -22,7 +22,10 @@
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
+import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.oauth2.AccessToken;
+import io.vertx.ext.auth.oauth2.impl.OAuth2AuthProviderImpl;
+import io.vertx.ext.jwt.JWT;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
@@ -53,13 +56,22 @@ public SecurityIdentity get() {
return authenticate(request, vertxContext);
}
- @SuppressWarnings("deprecation")
private CompletableFuture authenticate(TokenAuthenticationRequest request,
RoutingContext vertxContext) {
- CompletableFuture result = new CompletableFuture<>();
TenantConfigContext resolvedContext = tenantResolver.resolve(vertxContext, true);
- OidcTenantConfig config = resolvedContext.oidcConfig;
+ if (resolvedContext.oidcConfig.publicKey.isPresent()) {
+ return validateTokenWithoutOidcServer(request, resolvedContext);
+ } else {
+ return validateTokenWithOidcServer(request, resolvedContext);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private CompletableFuture validateTokenWithOidcServer(TokenAuthenticationRequest request,
+ TenantConfigContext resolvedContext) {
+
+ CompletableFuture result = new CompletableFuture<>();
resolvedContext.auth.decodeToken(request.getToken().getToken(),
new Handler>() {
@Override
@@ -68,42 +80,73 @@ public void handle(AsyncResult event) {
result.completeExceptionally(new AuthenticationFailedException(event.cause()));
return;
}
- AccessToken token = event.result();
- try {
- OidcUtils.validateClaims(config.getToken(), token.accessToken());
- } catch (OIDCException e) {
- result.completeExceptionally(new AuthenticationFailedException(e));
- return;
- }
-
- QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
- builder.addCredential(request.getToken());
-
- JsonWebToken jwtPrincipal;
- try {
- JwtClaims jwtClaims = JwtClaims.parse(token.accessToken().encode());
- jwtClaims.setClaim(Claims.raw_token.name(), request.getToken().getToken());
- jwtPrincipal = new OidcJwtCallerPrincipal(jwtClaims, request.getToken(),
- config.token.principalClaim.isPresent() ? config.token.principalClaim.get() : null);
- } catch (InvalidJwtException e) {
- result.completeExceptionally(new AuthenticationFailedException(e));
- return;
- }
- builder.setPrincipal(jwtPrincipal);
+ JsonObject tokenJson = event.result().accessToken();
try {
- String clientId = config.getClientId().isPresent() ? config.getClientId().get() : null;
- for (String role : OidcUtils.findRoles(clientId, config.getRoles(), token.accessToken())) {
- builder.addRole(role);
- }
- } catch (Exception e) {
- result.completeExceptionally(new ForbiddenException(e));
- return;
+ result.complete(validateAndCreateIdentity(request, resolvedContext.oidcConfig, tokenJson));
+ } catch (Throwable ex) {
+ result.completeExceptionally(ex);
}
-
- result.complete(builder.build());
}
});
return result;
}
+
+ private CompletableFuture validateTokenWithoutOidcServer(TokenAuthenticationRequest request,
+ TenantConfigContext resolvedContext) {
+ CompletableFuture result = new CompletableFuture<>();
+
+ OAuth2AuthProviderImpl auth = ((OAuth2AuthProviderImpl) resolvedContext.auth);
+ JWT jwt = auth.getJWT();
+ JsonObject tokenJson = null;
+ try {
+ tokenJson = jwt.decode(request.getToken().getToken());
+ } catch (Throwable ex) {
+ result.completeExceptionally(new AuthenticationFailedException(ex));
+ return result;
+ }
+ if (jwt.isExpired(tokenJson, auth.getConfig().getJWTOptions())) {
+ result.completeExceptionally(new AuthenticationFailedException());
+ } else {
+ try {
+ result.complete(validateAndCreateIdentity(request, resolvedContext.oidcConfig, tokenJson));
+ } catch (Throwable ex) {
+ result.completeExceptionally(ex);
+ }
+ }
+ return result;
+ }
+
+ private QuarkusSecurityIdentity validateAndCreateIdentity(TokenAuthenticationRequest request,
+ OidcTenantConfig config, JsonObject tokenJson)
+ throws Exception {
+ try {
+ OidcUtils.validateClaims(config.getToken(), tokenJson);
+ } catch (OIDCException e) {
+ throw new AuthenticationFailedException(e);
+ }
+
+ QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();
+ builder.addCredential(request.getToken());
+
+ JsonWebToken jwtPrincipal;
+ try {
+ JwtClaims jwtClaims = JwtClaims.parse(tokenJson.encode());
+ jwtClaims.setClaim(Claims.raw_token.name(), request.getToken().getToken());
+ jwtPrincipal = new OidcJwtCallerPrincipal(jwtClaims, request.getToken(),
+ config.token.principalClaim.isPresent() ? config.token.principalClaim.get() : null);
+ } catch (InvalidJwtException e) {
+ throw new AuthenticationFailedException(e);
+ }
+ builder.setPrincipal(jwtPrincipal);
+ try {
+ String clientId = config.getClientId().isPresent() ? config.getClientId().get() : null;
+ for (String role : OidcUtils.findRoles(clientId, config.getRoles(), tokenJson)) {
+ builder.addRole(role);
+ }
+ } catch (Exception e) {
+ throw new ForbiddenException(e);
+ }
+ return builder.build();
+ }
}
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java
index ed68de52aef70..5f356e7de5682 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java
@@ -11,6 +11,7 @@
import io.quarkus.arc.runtime.BeanContainer;
import io.quarkus.oidc.OIDCException;
+import io.quarkus.oidc.runtime.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.runtime.OidcTenantConfig.Credentials;
import io.quarkus.oidc.runtime.OidcTenantConfig.Credentials.Secret;
import io.quarkus.runtime.annotations.Recorder;
@@ -21,6 +22,7 @@
import io.vertx.ext.auth.PubSecKeyOptions;
import io.vertx.ext.auth.oauth2.OAuth2Auth;
import io.vertx.ext.auth.oauth2.OAuth2ClientOptions;
+import io.vertx.ext.auth.oauth2.impl.OAuth2AuthProviderImpl;
import io.vertx.ext.auth.oauth2.providers.KeycloakAuth;
import io.vertx.ext.jwt.JWTOptions;
@@ -65,13 +67,41 @@ private TenantConfigContext createTenantContext(Vertx vertx, OidcTenantConfig oi
return null;
}
+ OAuth2ClientOptions options = new OAuth2ClientOptions();
+
+ if (oidcConfig.getClientId().isPresent()) {
+ options.setClientID(oidcConfig.getClientId().get());
+ }
+
+ if (oidcConfig.getToken().issuer.isPresent()) {
+ options.setValidateIssuer(false);
+ }
+
+ if (oidcConfig.getToken().getExpirationGrace().isPresent()) {
+ JWTOptions jwtOptions = new JWTOptions();
+ jwtOptions.setLeeway(oidcConfig.getToken().getExpirationGrace().get());
+ options.setJWTOptions(jwtOptions);
+ }
+
+ if (oidcConfig.getPublicKey().isPresent()) {
+ if (oidcConfig.applicationType == ApplicationType.WEB_APP) {
+ throw new ConfigurationException("'public-key' property can only be used with the 'service' applications");
+ }
+ LOG.info("'public-key' property for the local token verification is set,"
+ + " no connection to the OIDC server will be created");
+ options.addPubSecKey(new PubSecKeyOptions()
+ .setAlgorithm("RS256")
+ .setPublicKey(oidcConfig.getPublicKey().get()));
+
+ return new TenantConfigContext(new OAuth2AuthProviderImpl(vertx, options), oidcConfig);
+ }
+
if (!oidcConfig.getAuthServerUrl().isPresent() || !oidcConfig.getClientId().isPresent()) {
throw new ConfigurationException(
- "auth-server-url and client-id must be configured when the quarkus-oidc extension is enabled");
+ "Both 'auth-server-url' and 'client-id' or alterntively 'public-key' must be configured"
+ + " when the quarkus-oidc extension is enabled");
}
- OAuth2ClientOptions options = new OAuth2ClientOptions();
-
// Base IDP server URL
options.setSite(oidcConfig.getAuthServerUrl().get());
// RFC7662 introspection service address
@@ -84,10 +114,6 @@ private TenantConfigContext createTenantContext(Vertx vertx, OidcTenantConfig oi
options.setJwkPath(oidcConfig.getJwksPath().get());
}
- if (oidcConfig.getClientId().isPresent()) {
- options.setClientID(oidcConfig.getClientId().get());
- }
-
Credentials creds = oidcConfig.getCredentials();
if (creds.secret.isPresent() && (creds.clientSecret.value.isPresent() || creds.clientSecret.method.isPresent())) {
throw new ConfigurationException(
@@ -95,7 +121,6 @@ private TenantConfigContext createTenantContext(Vertx vertx, OidcTenantConfig oi
}
// TODO: The workaround to support client_secret_post is added below and have to be removed once
// it is supported again in VertX OAuth2.
-
if (creds.secret.isPresent()
|| creds.clientSecret.value.isPresent()
&& creds.clientSecret.method.orElseGet(() -> Secret.Method.BASIC) == Secret.Method.BASIC) {
@@ -107,21 +132,6 @@ private TenantConfigContext createTenantContext(Vertx vertx, OidcTenantConfig oi
options.setClientSecretParameterName(null);
}
- if (oidcConfig.getPublicKey().isPresent()) {
- options.addPubSecKey(new PubSecKeyOptions()
- .setAlgorithm("RS256")
- .setPublicKey(oidcConfig.getPublicKey().get()));
- }
- if (oidcConfig.getToken().issuer.isPresent()) {
- options.setValidateIssuer(false);
- }
-
- if (oidcConfig.getToken().getExpirationGrace().isPresent()) {
- JWTOptions jwtOptions = new JWTOptions();
- jwtOptions.setLeeway(oidcConfig.getToken().getExpirationGrace().get());
- options.setJWTOptions(jwtOptions);
- }
-
final long connectionDelayInSecs = oidcConfig.getConnectionDelay().isPresent()
? oidcConfig.getConnectionDelay().get().toMillis() / 1000
: 0;