Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REST resource extending abstract class fail on POST/PUT scenario #253

Merged
merged 1 commit into from
Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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();
}
}