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

WebSockets Next: Support for secure upgrade with security annotations only #40957

Conversation

michalvavrik
Copy link
Member

closes: #40622

This comment has been minimized.

@michalvavrik michalvavrik force-pushed the feature/websockets-next-eager-sec-checks branch 2 times, most recently from 3818f2a to b02154a Compare June 4, 2024 11:19
Copy link

github-actions bot commented Jun 4, 2024

🙈 The PR is closed and the preview is expired.

This comment has been minimized.

Copy link
Member

@cescoffier cescoffier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea looks good.

Might be unrelated to this PR, but how can a user implement sub-protocol negotiation?

This comment has been minimized.

Copy link
Contributor

@mkouba mkouba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot review the security parts but the rest looks good.

@mkouba
Copy link
Contributor

mkouba commented Jun 5, 2024

Might be unrelated to this PR, but how can a user implement sub-protocol negotiation?

@cescoffier First of all, we should describe the current subprotol support. The server can configure a list of supported subprotocols (globally, not per-endpoint). Now if a client sends the Sec-WebSocket-Protocol HTTP header in the upgrade request and none of the requested subprotocol is available then the connection is rejected. If the requested subprotocol is available the connection is established and the selected subprotocol is accessible via WebSocketConnection#subprotocol(). I'm not so sure what happens if multiple subprotocols match. Probably the first one is selected.

With a custom HttpUpgradeCheck the application can inspect/modify the Sec-WebSocket-Protocol header and/or reject the connection immediately, e.g. before the upgrade is perfomed. However, it is not possbile to set the selected subprotocol on the connection, i.e. WebSocketConnection#subprotocol() may return null if quarkus.websockets-next.server.supported-subprotocols is empty.

@michalvavrik michalvavrik force-pushed the feature/websockets-next-eager-sec-checks branch from b02154a to 5332010 Compare June 5, 2024 10:47
Copy link

quarkus-bot bot commented Jun 5, 2024

Status for workflow Quarkus Documentation CI

This is the status report for running Quarkus Documentation CI on commit 5332010.

✅ The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

⚠️ There are other workflow runs running, you probably need to wait for their status before merging.

Copy link

quarkus-bot bot commented Jun 5, 2024

Status for workflow Quarkus CI

This is the status report for running Quarkus CI on commit 5332010.

✅ The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the Develocity build scans.


Flaky tests - Develocity

⚙️ JVM Tests - JDK 17

📦 integration-tests/reactive-messaging-kafka

io.quarkus.it.kafka.KafkaConnectorTest.testFruits - History

  • Assertion condition defined as a Lambda expression in io.quarkus.it.kafka.KafkaConnectorTest expected: <6> but was: <5> within 10 seconds. - org.awaitility.core.ConditionTimeoutException
org.awaitility.core.ConditionTimeoutException: Assertion condition defined as a Lambda expression in io.quarkus.it.kafka.KafkaConnectorTest expected: <6> but was: <5> within 10 seconds.
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:119)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:31)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1006)
	at org.awaitility.core.ConditionFactory.untilAsserted(ConditionFactory.java:790)
	at io.quarkus.it.kafka.KafkaConnectorTest.testFruits(KafkaConnectorTest.java:63)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)

⚙️ JVM Tests - JDK 21

📦 integration-tests/reactive-messaging-kafka

io.quarkus.it.kafka.KafkaConnectorTest.testFruits - History

  • Assertion condition defined as a Lambda expression in io.quarkus.it.kafka.KafkaConnectorTest expected: <6> but was: <5> within 10 seconds. - org.awaitility.core.ConditionTimeoutException
org.awaitility.core.ConditionTimeoutException: Assertion condition defined as a Lambda expression in io.quarkus.it.kafka.KafkaConnectorTest expected: <6> but was: <5> within 10 seconds.
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:119)
	at org.awaitility.core.AssertionCondition.await(AssertionCondition.java:31)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1006)
	at org.awaitility.core.ConditionFactory.untilAsserted(ConditionFactory.java:790)
	at io.quarkus.it.kafka.KafkaConnectorTest.testFruits(KafkaConnectorTest.java:63)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)

@sberyozkin
Copy link
Member

Thanks @michalvavrik, it looks fine but a bit complex to review from PTO 🙂, so will do next week

@mkouba
Copy link
Contributor

mkouba commented Jun 11, 2024

@sberyozkin ping. Did you get a chance to review the security parts?


Other options for securing HTTP upgrade requests, such as using the security annotations, will be explored in the future.
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michalvavrik So, in the example above, open() is secured and echo is not ? It would be unexpected IMHO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the example above "open" and "echo" are not secured, but HTTP upgrade is secured. The HTTP upgrade happens before "open" and "echo", therefore they are only not secured if you inject "Endpoint" class as a bean into another CDI bean and call these methods directly.

That is:

@Inject
Endpoint endpoint

public void do() {
   endpoint.open();
}

if we were to secure echo, it would mean for every single message is performed authorization despite known result (you know it will pass because the HTTP upgrade is secured).

I can point you to tests that asserts security is not relaxed where not documented.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michalvavrik Michal, these are low level technical details that HTTP upgrade happens before onOpen, to me, on onOpen actually represents a successful HTTP upgrade completion, even if it is not quite a correct picture.

Let me clarify how it aligns with #40534.

So there we have an HTTP security policy securing an upgrade:

quarkus.http.auth.permission.secured.paths=/end
quarkus.http.auth.permission.secured.policy=authenticated

and then we have

@WebSocket(path = "/end")
public class Endpoint {
    @Inject
    SecurityIdentity currentIdentity;
    
    @RolesAllowed("admin")
    @OnTextMessage
    String echo(String message) {
        return message;
    }
}

so @RolesAllowed must be placed on the echo method to ensure the current security identity is present and matches the sec constraint before echo is called.

Let's say, with your PR, I only want that echo is accessed by the non-anonymous security identity.

Are you saying we must do:

@WebSocket(path = "/end")
@Authenticated
public class Endpoint {
    @Inject
    SecurityIdentity currentIdentity;
    
    @Authenticated
    @OnTextMessage
    String echo(String message) {
        return message;
    }
}

to have echo secured ?

Also, does

@WebSocket(path = "/end")
@RolesAllowed("admin")
public class Endpoint {
 
}

works for a secure HTTP upgrade ? And if yes, should users also duplicate @RolesAllowed("admin") on the echo method for the echo be secured ?

I guess, all I'm asking, why can't we have a single @Authenticated at the class level, and not require users duplicate across various methods like onOpen, onEcho, etc, if all users want is, for ex, an authenticated access ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying we must do:

@WebSocket(path = "/end")
@Authenticated
public class Endpoint {
    @Inject
    SecurityIdentity currentIdentity;
    
    @Authenticated
    @OnTextMessage
    String echo(String message) {
        return message;
    }
}

to have echo secured ?

Nope, @Authenticated declared on an endpoint means that no callback can be called for non-anonymous security identity. In other words, we secure the HTTP upgrade and if the check fails then no method declared on the endpoint is invoked.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, does

@WebSocket(path = "/end")
@RolesAllowed("admin")
public class Endpoint {
 
}

works for a secure HTTP upgrade ? And if yes, should users also duplicate @RolesAllowed("admin") on the echo method for the echo be secured ?

It's the same principle, i.e. no need to duplicate the annotation.

Copy link
Member Author

@michalvavrik michalvavrik Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you suggest a message @sberyozkin ? It's optional, but I'd appreciate it. Otherwise I'll try to rewrite it, but I don't know if it will fit your suggestion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michalvavrik

IMHO noone will inject endpoint and use it as CDI bean

May be this is the source of the confusion on my behalf and simply dropping this message will help ? See, I'm not thinking as a user in terms of CDI beans, when I see an example like

@WebSocket(path = "/end")
@Authenticated
public class Endpoint {
    @Inject
    SecurityIdentity currentIdentity;
    
    @OnTextMessage
    String echo(String message) {
        return message;
    }
}

I expect that the callback echo and onOpen methods are secured, I don't want to inject this endpoint anywhere, it is like a JAX-RS resource, we don't inject it somewhere else...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which is why reading Placing a security annotation on an endpoint bean will not secure bean methods, only the HTTP upgrade. confuses me a lot

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, I don't consider it as a blocker and minor updates to docs can be backported later if agreed to, so @mkouba @gsmet please merge if all looks good to you otherwise

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think small follow-up docs PR where we can iterate would be better than do it on this PR as CI takes longer and I think we need more than attempt. +1 to merge this

@sberyozkin sberyozkin self-requested a review June 11, 2024 15:48
Copy link
Member

@sberyozkin sberyozkin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, please merge for this PR to make it to 3.12.

I suggest to rework the message related to the securing the upgrade vs the onOpen/onTextMesage callbacks if possible, but is not a requirement for this PR

@sberyozkin
Copy link
Member

OK, let me merge it myself then

@sberyozkin sberyozkin merged commit b8994ac into quarkusio:main Jun 11, 2024
55 checks passed
@quarkus-bot quarkus-bot bot added this to the 3.12 - main milestone Jun 11, 2024
@quarkus-bot quarkus-bot bot added the kind/enhancement New feature or request label Jun 11, 2024
@michalvavrik michalvavrik deleted the feature/websockets-next-eager-sec-checks branch June 11, 2024 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

Successfully merging this pull request may close these issues.

WebSockets Next: Support for secure upgrade with security annotations only
4 participants