Skip to content

Commit

Permalink
REST resource extending abstract class fail on POST/PUT scenario
Browse files Browse the repository at this point in the history
  • Loading branch information
pablo gonzalez granados committed Sep 10, 2021
1 parent deb2ab2 commit fa75513
Show file tree
Hide file tree
Showing 14 changed files with 550 additions and 0 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,14 @@ Authorization is based on URL patterns, and Keycloak is used for defining and en

A simple Keycloak realm with 1 client (protected application), 2 users, 2 roles and 2 protected resources is provided in `test-realm.json`.

### `security/keycloak-authz-reactive-resteasy`
QUARKUS-1257 - Verifies authenticated endpoints with a generic body in parent class
Verifies token-based authn and URL-based authz.
Authentication is OIDC, and Keycloak is used for issuing and verifying tokens.
Authorization is based on URL patterns, and Keycloak is used for defining and enforcing restrictions.

A simple Keycloak realm with 1 client (protected application), 2 users, 2 roles and 3 protected resources is provided in `test-realm.json`.

### `security/keycloak-webapp`

Verifies authorization code flow and role-based authentication to protect web applications.
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@
<module>security/keycloak-oidc-client-basic</module>
<module>security/keycloak-oidc-client-extended</module>
<module>security/vertx-jwt</module>
<module>security/keycloak-authz-reactive-resteasy</module>
</modules>
</profile>
<profile>
Expand Down
60 changes: 60 additions & 0 deletions security/keycloak-authz-reactive-resteasy/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?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>
<parent>
<groupId>io.quarkus.ts.qe</groupId>
<artifactId>parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../..</relativePath>
</parent>
<artifactId>security-keycloak-authz-reactive-resteasy</artifactId>
<packaging>jar</packaging>
<name>Quarkus QE TS: Security: Keycloak + Authorization + reactive resteasy</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-authorization</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus.qe</groupId>
<artifactId>quarkus-test-service-keycloak</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<!-- Skipped on Windows as does not support Linux Containers / Testcontainers -->
<profile>
<id>skip-tests-on-windows</id>
<activation>
<os>
<family>windows</family>
</os>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkus.ts.security.keycloak.authz;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.security.identity.SecurityIdentity;
import io.smallrye.mutiny.Uni;

@Path("/admin")
public class AdminResource {
@Inject
SecurityIdentity identity;

@Inject
JsonWebToken jwt;

@GET
@Produces(MediaType.TEXT_PLAIN)
public Uni<String> get() {
return Uni.createFrom().item("Hello, admin " + identity.getPrincipal().getName());
}

@GET
@Path("/issuer")
@Produces(MediaType.TEXT_PLAIN)
public Uni<String> issuer() {
return Uni.createFrom().item("admin token issued by " + jwt.getIssuer());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.ts.security.keycloak.authz;

import javax.ws.rs.GET;
import javax.ws.rs.Path;

import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;

@Path("/user-details")
@Authenticated
public class UserAdvancedResource extends UserDetailsResource<String> {
@GET
public Uni<String> info() {
return Uni.createFrom().item(identity.getPrincipal().getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkus.ts.security.keycloak.authz;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.security.identity.SecurityIdentity;
import io.smallrye.mutiny.Uni;

public class UserDetailsResource<T> {

@Inject
SecurityIdentity identity;

@Path("/advanced")
@POST
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
public Uni<String> getUserName(T body) {
return Uni.createFrom().item(identity.getPrincipal().getName());
}

@Path("/advanced-specific")
@POST
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
public Uni<String> WebAuthnSpecific(String body) {
return Uni.createFrom().item(identity.getPrincipal().getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkus.ts.security.keycloak.authz;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.security.identity.SecurityIdentity;
import io.smallrye.mutiny.Uni;

@Path("/user")
public class UserResource {
@Inject
SecurityIdentity identity;

@Inject
JsonWebToken jwt;

@GET
@Produces(MediaType.TEXT_PLAIN)
public Uni<String> get() {
return Uni.createFrom().item("Hello, user " + identity.getPrincipal().getName());
}

@GET
@Path("/issuer")
@Produces(MediaType.TEXT_PLAIN)
public Uni<String> issuer() {
return Uni.createFrom().item("user token issued by " + jwt.getIssuer());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
quarkus.oidc.auth-server-url=${KEYCLOAK_HTTP_URL:http://localhost:8180}/auth/realms/test-realm
quarkus.oidc.client-id=test-application-client
quarkus.oidc.credentials.secret=test-application-client-secret
# tolerate 1 minute of clock skew between the Keycloak server and the application
quarkus.oidc.token.lifespan-grace=60
quarkus.keycloak.policy-enforcer.enable=true
quarkus.keycloak.policy-enforcer.paths.health.path=/q/*
quarkus.keycloak.policy-enforcer.paths.health.enforcement-mode=DISABLED
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package io.quarkus.ts.security.keycloak.authz;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;

import org.apache.http.HttpStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.util.StringUtils;
import org.keycloak.authorization.client.AuthzClient;

import io.quarkus.test.bootstrap.KeycloakService;
import io.quarkus.test.bootstrap.RestService;
import io.smallrye.mutiny.helpers.test.UniAssertSubscriber;
import io.vertx.core.http.HttpMethod;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;
import io.vertx.mutiny.ext.web.client.HttpResponse;

public abstract class BaseAuthzSecurityIT {

static final String NORMAL_USER = "test-normal-user";
static final String ADMIN_USER = "test-admin-user";
static final String REALM_DEFAULT = "test-realm";
static final String CLIENT_ID_DEFAULT = "test-application-client";
static final String CLIENT_SECRET_DEFAULT = "test-application-client-secret";

private AuthzClient authzClient;
private UniAssertSubscriber<HttpResponse<Buffer>> response;

@BeforeEach
public void setup() {
authzClient = getKeycloak().createAuthzClient(CLIENT_ID_DEFAULT, CLIENT_SECRET_DEFAULT);
}

@Tag("QUARKUS-1257")
@Test
public void genericAndExtendedSecuredEndpointShouldResponseOk() {
String bearerToken = getToken(NORMAL_USER, NORMAL_USER);

whenMakeRequestTo(HttpMethod.GET, "/user-details", bearerToken);
thenStatusCodeIs(HttpStatus.SC_OK);
thenBodyIs(NORMAL_USER);

whenMakeRequestTo(HttpMethod.POST, "/user-details/advanced-specific", bearerToken);
thenStatusCodeIs(HttpStatus.SC_OK);
thenBodyIs(NORMAL_USER);

whenMakeRequestTo(HttpMethod.POST, "/user-details/advanced", bearerToken);
thenStatusCodeIs(HttpStatus.SC_OK);
thenBodyIs(NORMAL_USER);
}

@Test
public void normalUserUserResource() {
whenMakeRequestTo(HttpMethod.GET, "/user", getToken(NORMAL_USER, NORMAL_USER));
thenStatusCodeIs(HttpStatus.SC_OK);
thenBodyIs("Hello, user " + NORMAL_USER);
}

@Test
public void normalUserUserResourceIssuer() {
whenMakeRequestTo(HttpMethod.GET, "/user/issuer", getToken(NORMAL_USER, NORMAL_USER));
thenStatusCodeIs(HttpStatus.SC_OK);
thenBodyStartWith("user token issued by " + getKeycloak().getHost());
}

@Test
public void normalUserAdminResource() {
whenMakeRequestTo(HttpMethod.GET, "/admin", getToken(NORMAL_USER, NORMAL_USER));
thenStatusCodeIs(HttpStatus.SC_FORBIDDEN);
}

@Test
public void adminUserUserResource() {
whenMakeRequestTo(HttpMethod.GET, "/user", getToken(ADMIN_USER, ADMIN_USER));
thenStatusCodeIs(HttpStatus.SC_OK);
thenBodyIs("Hello, user " + ADMIN_USER);
}

@Test
public void adminUserAdminResource() {
whenMakeRequestTo(HttpMethod.GET, "/admin", getToken(ADMIN_USER, ADMIN_USER));
thenStatusCodeIs(HttpStatus.SC_OK);
thenBodyIs("Hello, admin " + ADMIN_USER);
}

@Test
public void adminUserAdminResourceIssuer() {
whenMakeRequestTo(HttpMethod.GET, "/admin/issuer", getToken(ADMIN_USER, ADMIN_USER));
thenStatusCodeIs(HttpStatus.SC_OK);
thenBodyStartWith("admin token issued by " + getKeycloak().getHost());
}

@Test
public void noUserUserResource() {
whenMakeRequestTo(HttpMethod.GET, "/user", "");
thenStatusCodeIs(HttpStatus.SC_UNAUTHORIZED);
}

@Test
public void noUserAdminResource() {
whenMakeRequestTo(HttpMethod.GET, "/admin", "");
thenStatusCodeIs(HttpStatus.SC_UNAUTHORIZED);
}

private void thenBodyIs(String expectedBody) {
String bodyAsString = response.await().assertCompleted().getItem().bodyAsString();
assertThat(expectedBody, equalTo(bodyAsString));
}

private void thenBodyStartWith(String expectedBody) {
String bodyAsString = response.await().assertCompleted().getItem().bodyAsString();
assertThat(bodyAsString, startsWith(expectedBody));
}

private void thenStatusCodeIs(int expectedCode) {
int statusCode = response.await().assertCompleted().getItem().statusCode();
assertThat(expectedCode, equalTo(statusCode));
}

private void whenMakeRequestTo(HttpMethod method, String path, String bearerToken) {
HttpRequest<Buffer> req = createHttpRequest(method, path);
if (StringUtils.isNotBlank(bearerToken)) {
addHttpRequestBearerToken(req, bearerToken);
}

response = req.send().subscribe().withSubscriber(UniAssertSubscriber.create());
}

private HttpRequest<Buffer> createHttpRequest(HttpMethod method, String path) {
return getApp().mutiny().request(method, path);
}

private HttpRequest<Buffer> addHttpRequestBearerToken(HttpRequest<Buffer> req, String token) {
return req.bearerTokenAuthentication(token);
}

protected abstract KeycloakService getKeycloak();

protected abstract RestService getApp();

private String getToken(String userName, String password) {
return authzClient.obtainAccessToken(userName, password).getToken();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.ts.security.keycloak.authz;

import io.quarkus.test.bootstrap.KeycloakService;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.Container;
import io.quarkus.test.services.QuarkusApplication;

@QuarkusScenario
public class KeycloakAuthzSecurityIT extends BaseAuthzSecurityIT {

static final int KEYCLOAK_PORT = 8080;

@Container(image = "${keycloak.image}", expectedLog = "Http management interface listening", port = KEYCLOAK_PORT)
static KeycloakService keycloak = new KeycloakService("/keycloak-realm.json", REALM_DEFAULT);

@QuarkusApplication
static RestService app = new RestService()
.withProperty("quarkus.oidc.auth-server-url", () -> keycloak.getRealmUrl())
.withProperty("quarkus.oidc.client-id", CLIENT_ID_DEFAULT)
.withProperty("quarkus.oidc.credentials.secret", CLIENT_SECRET_DEFAULT);

@Override
protected KeycloakService getKeycloak() {
return keycloak;
}

@Override
protected RestService getApp() {
return app;
}
}

0 comments on commit fa75513

Please sign in to comment.