Skip to content

Commit

Permalink
Support permission checkers for WebSockets Next
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Jan 20, 2025
1 parent 7d65aa3 commit 4ce0937
Show file tree
Hide file tree
Showing 8 changed files with 797 additions and 65 deletions.
174 changes: 142 additions & 32 deletions docs/src/main/asciidoc/websockets-next-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,12 @@ public class MyWebSocket {

==== Request context

If an endpoint is annotated with `@RequestScoped`, or with a security annotation (such as `@RolesAllowed`), or depends directly or indirectly on a `@RequestScoped` bean, or on a bean annotated with a security annotation, then each WebSocket endpoint callback method execution is associated with a new _request context_.
The request context is active during endpoint callback invocation.
Each WebSocket endpoint callback method execution is associated with a new _request context_ (activate CDI request context) if the endpoint is:

* Annotated with the `@RequestScoped` annotation.
* Has a method annotated with a security annotation such as `@RolesAllowed`.
* Depends directly or indirectly on a `@RequestScoped` bean.
* Depends directly or indirectly on a CDI beans secured with a standard security annotation.

TIP: It is also possible to set the `quarkus.websockets-next.server.activate-request-context` config property to `always`. In this case, the request context is always activated when an endpoint callback is invoked.

Expand Down Expand Up @@ -783,6 +787,64 @@ class MyBean {
[[websocket-next-security]]
=== Security

`SecurityIdentity` is initially created during a secure HTTP upgrade and associated with the websocket connection.

NOTE: When OpenID Connect extension is used and token expires, Quarkus automatically closes connection.

[[secure-http-upgrade]]
==== Secure HTTP upgrade

An HTTP upgrade is secured when standard security annotation is placed on an endpoint class or an HTTP Security policy is defined.
The advantage of securing HTTP upgrade is less processing, the authorization is performed early and only once.
You should always prefer HTTP upgrade security unless, like in the example above, you need to perform an action on error or a security check based on the payload.

.Use standard security annotation to secure an HTTP upgrade
[source, java]
----
package io.quarkus.websockets.next.test.security;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
@Authenticated <1>
@WebSocket(path = "/end")
public class Endpoint {
@Inject
SecurityIdentity currentIdentity;
@OnOpen
String open() {
return "ready";
}
@OnTextMessage
String echo(String message) {
return message;
}
}
----
<1> Initial HTTP handshake ends with the 401 status for anonymous users.
You can also redirect the handshake request on authorization failure with the `quarkus.websockets-next.server.security.auth-failure-redirect-url` configuration property.

IMPORTANT: HTTP upgrade is only secured when a security annotation is declared on an endpoint class next to the `@WebSocket` annotation.
Placing a security annotation on an endpoint bean will not secure bean methods, only the HTTP upgrade.
You must always verify that your endpoint is secured as intended.

.Use HTTP Security policy to secure an HTTP upgrade
[source,properties]
----
quarkus.http.auth.permission.http-upgrade.paths=/end
quarkus.http.auth.permission.http-upgrade.policy=authenticated
----

==== Secure WebSocket endpoint callback methods

WebSocket endpoint callback methods can be secured with security annotations such as `io.quarkus.security.Authenticated`,
`jakarta.annotation.security.RolesAllowed` and other annotations listed in the xref:security-authorize-web-endpoints-reference.adoc#standard-security-annotations[Supported security annotations] documentation.

Expand Down Expand Up @@ -828,60 +890,108 @@ public class Endpoint {
<1> The echo callback method can only be invoked if the current security identity has an `admin` role.
<2> The error handler is invoked in case of the authorization failure.

`SecurityIdentity` is initially created during a secure HTTP upgrade and associated with the websocket connection.
==== Secure server endpoints with permission checkers

NOTE: When OpenID Connect extension is used and token expires, Quarkus automatically closes connection.
WebSocket endpoints can be secured with the xref:security-authorize-web-endpoints-reference.adoc#permission-checker[permission checkers].
We recommend to <<secure-http-upgrade>> rather than individual endpoint methods. For example:

=== Secure HTTP upgrade
.Example of a WebSocket endpoint with secured HTTP upgrade
[source, java]
----
package io.quarkus.websockets.next.test.security;
An HTTP upgrade is secured when standard security annotation is placed on an endpoint class or an HTTP Security policy is defined.
The advantage of securing HTTP upgrade is less processing, the authorization is performed early and only once.
You should always prefer HTTP upgrade security unless, like in th example above, you need to perform action on error.
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
.Use standard security annotation to secure an HTTP upgrade
@PermissionsAllowed("product:premium")
@WebSocket(path = "/product/premium")
public class PremiumProductEndpoint {
@OnTextMessage
PremiumProduct getPremiumProduct(int productId) {
return new PremiumProduct(productId);
}
}
----

.Example of a permission checker authorizing the HTTP upgrade
[source, java]
----
package io.quarkus.websockets.next.test.security;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.PermissionChecker;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class PermissionChecker {
@PermissionChecker("product:premium")
public boolean canGetPremiumProduct(SecurityIdentity securityIdentity) { <1>
String username = currentIdentity.getPrincipal().getName();
RoutingContext routingContext = HttpSecurityUtils.getRoutingContextAttribute(securityIdentity);
String initialHttpUpgradePath = routingContext == null ? null : routingContext.normalizedPath();
if (!isUserAllowedToAccessPath(initialHttpUpgradePath, username)) {
return false;
}
return isPremiumCustomer(username);
}
}
----
<1> A permission checker authorizing an HTTP upgrade must declare exactly one method parameter, the `SecurityIdentity`.

If the security check performs payload-aware authorization, it is also possible to run check on every message. For example:

[source, java]
----
package io.quarkus.websockets.next.test.security;
import io.quarkus.security.PermissionChecker;
import io.quarkus.security.PermissionsAllowed;
import jakarta.inject.Inject;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.websockets.next.OnError;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
@Authenticated <1>
@WebSocket(path = "/end")
public class Endpoint {
@WebSocket(path = "/product")
public class ProductEndpoint {
private record Product(int id, String name) {}
@Inject
SecurityIdentity currentIdentity;
@OnOpen
String open() {
return "ready";
}
@PermissionsAllowed("product:get")
@OnTextMessage
String echo(String message) {
return message;
Product getProduct(int productId) { <1>
return new Product(productId, "Product " + productId);
}
}
----
<1> Initial HTTP handshake ends with the 401 status for anonymous users.
You can also redirect the handshake request on authorization failure with the `quarkus.websockets-next.server.security.auth-failure-redirect-url` configuration property.
IMPORTANT: HTTP upgrade is only secured when a security annotation is declared on an endpoint class next to the `@WebSocket` annotation.
Placing a security annotation on an endpoint bean will not secure bean methods, only the HTTP upgrade.
You must always verify that your endpoint is secured as intended.
@OnError
String error(ForbiddenException t) { <2>
return "forbidden:" + currentIdentity.getPrincipal().getName();
}
.Use HTTP Security policy to secure an HTTP upgrade
[source,properties]
----
quarkus.http.auth.permission.http-upgrade.paths=/end
quarkus.http.auth.permission.http-upgrade.policy=authenticated
@PermissionChecker("product:get")
boolean canGetProduct(int productId) {
String username = currentIdentity.getPrincipal().getName();
return currentIdentity.hasRole("admin") || canUserGetProduct(productId, username);
}
}
----
<1> The `getProduct` callback method can only be invoked if the current security identity has an `admin` role or the user is allowed to get the product detail.
<2> The error handler is invoked in case of the authorization failure.

=== Inspect and/or reject HTTP upgrade

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public List<AnnotationInstance> getTransitiveInstances() {
return transitiveInstances;
}

private boolean hasPermissionsAllowed(List<AnnotationInstance> instances) {
public boolean hasPermissionsAllowed(List<AnnotationInstance> instances) {
return instances.stream().anyMatch(ai -> metaAnnotationNames.contains(ai.name()));
}

Expand Down
Loading

0 comments on commit 4ce0937

Please sign in to comment.