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
Add new scenario in order to cover reactive cases
Rename security/keycloak-authz module to security/keycloak-authz-classic
  • Loading branch information
pablo gonzalez granados authored and Sgitario committed Sep 10, 2021
1 parent deb2ab2 commit 35e2ac7
Show file tree
Hide file tree
Showing 24 changed files with 553 additions and 3 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,22 @@ Restrictions are defined using common annotations (`@RolesAllowed` etc.).

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

### `security/keycloak-authz`
### `security/keycloak-authz-classic`

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 2 protected resources is provided in `test-realm.json`.

### `security/keycloak-authz-reactive`
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
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@
<module>security/https</module>
<module>security/jwt</module>
<module>security/keycloak</module>
<module>security/keycloak-authz</module>
<module>security/keycloak-authz-classic</module>
<module>security/keycloak-authz-reactive</module>
<module>security/keycloak-jwt</module>
<module>security/keycloak-webapp</module>
<module>security/keycloak-oauth2</module>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</parent>
<artifactId>security-keycloak-authz</artifactId>
<packaging>jar</packaging>
<name>Quarkus QE TS: Security: Keycloak + Authorization</name>
<name>Quarkus QE TS: Security: Keycloak + Authorization + Classic</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
Expand Down
60 changes: 60 additions & 0 deletions security/keycloak-authz-reactive/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</artifactId>
<packaging>jar</packaging>
<name>Quarkus QE TS: Security: Keycloak + Authorization + Reactive</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();
}
}

0 comments on commit 35e2ac7

Please sign in to comment.