Skip to content

WebSocket Next: enable users to update SecurityIdentity before previous bearer access token expires #47675

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

michalvavrik
Copy link
Member

This comment has been minimized.

Copy link

github-actions bot commented May 3, 2025

🎊 PR Preview 12c75e0 has been successfully built and deployed to https://quarkus-pr-main-47675-preview.surge.sh/version/main/guides/

  • Images of blog posts older than 3 months are not available.
  • Newsletters older than 3 months are not available.

This comment has been minimized.

@michalvavrik michalvavrik requested review from sberyozkin and mkouba and removed request for sberyozkin May 3, 2025 13:05
@sberyozkin
Copy link
Member

@michalvavrik

I have more plans for WS Next and while I didn't mention it (no reason to mention it), what I have added to this PR will allow me to easily fix quarkiverse/quarkus-langchain4j#1418 in the follow up.

If you can see a way to do the work to support Quarkus LangChain4j fix without having to support the token injection, then please consider a dedicated PR - simply because, if that is considered a bug fix, it can be backported to 3.20 LTS, as this PR is unlikely to be backported

@michalvavrik
Copy link
Member Author

If you can see a way to do the work to support Quarkus LangChain4j fix without having to support the token injection, then please consider a dedicated PR - simply because, if that is considered a bug fix, it can be backported to 3.20 LTS, as this PR is unlikely to be backported

My plan is to use the fact that this PR stores security support on the connection. It should fix it inherently, as either LangChain4j uses the same duplicate context where we have the connection, or a new duplicated context created from the original one (there is a hierarchy and this will be parent one).

Anyway, I don't think it is important, the user that reported it didn't bother to add reproducer so I can't prove it is actual fix and fixing it after this get in is ok as this could be very edge case. Also, the way I plan to propose it (it may never get it), it could possibly provide further improvements, but not sort of code changes you want to backport. It is just idea, maybe never gets in.

@sberyozkin
Copy link
Member

@michalvavrik Once the sample SPA sending a bearer token to HTTP upgrade is available, you can use it as a reproducer for the Quarkus LangChain4j issue too, so overall, it is worth it, not only to check the PR idea works, but also as a base for StepUp authentication and other experiments

@michalvavrik
Copy link
Member Author

@michalvavrik Once the sample SPA sending a bearer token to HTTP upgrade is available, you can use it as a reproducer for the Quarkus LangChain4j issue too, so overall, it is worth it, not only to check the PR idea works, but also as a base for StepUp authentication and other experiments

Here is working example quarkusio/quarkus-quickstarts#1534.

This comment has been minimized.

This comment has been minimized.

@michalvavrik michalvavrik requested a review from sberyozkin May 25, 2025 07:39
AuthenticationRequestContext authenticationRequestContext) {
return authenticate(request.getCredential().getToken(), getRoutingContextAttribute(request))
.onItem().transformToUni(newIdentity -> {
TokenIntrospection introspection = OidcUtils.getAttribute(newIdentity, OidcUtils.INTROSPECTION_ATTRIBUTE);
Copy link
Member

Choose a reason for hiding this comment

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

@michalvavrik I think we should start with enforcing that the sub is available, either via TokenIntrospection or JWT. You can check if it is JWT checking if newIdentity.getPrincipal() is an instance of JsonWebToken - if it is, the sub claim must be available, if not, it must be in the introspection response.
That may not work with OAuth2 providers, but I believe we should start in a strict mode.

Copy link
Member Author

Choose a reason for hiding this comment

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

The sub claim check is now enforced.

@sberyozkin
Copy link
Member

@michalvavrik

Here is working example quarkusio/quarkus-quickstarts#1534.

This is very useful to have, it is easier to understand this PR with it for sure.

@sberyozkin sberyozkin requested a review from cescoffier May 27, 2025 09:19
@sberyozkin
Copy link
Member

I'd also appreciate if @cescoffier could have a look.

Clement, I think this PR is good, the idea is that the expired token associated with the WS connection is optionally updated with the refreshed token on the same connection, without closing it. Otherwise, when the token expires, the connection is closed.

My only remaining concern is that the bearer access token is updated on the WS connection via the front-channel, with the SPA forwarding it to Quarkus over the current WS connection. With quarkus-oidc authorization code flow, if the refresh token happens, it is driven by Quarkus itself, avoiding the browser.

I can't imagine right now any risks. The token refreshed on the connection undergoes the same security checks, and is also compared against the previous identity using a unique sub key.

Please think about it when you get some time, also CC Martin

@michalvavrik
Copy link
Member Author

My only remaining concern is that the bearer access token is updated on the WS connection via the front-channel, with the SPA forwarding it to Quarkus over the current WS connection. With quarkus-oidc authorization code flow, if the refresh token happens, it is driven by Quarkus itself, avoiding the browser.

We can't access HttpOnly session cookie, therefore using Quarkus OIDC authorization code flow would require different mechanism, like once you are authenticated, return some unique one-time short-lived token that can be passed when the connection is opened and this way, we can link the OIDC session with newly opened WS connection. I think it requires a new Quarkus enhancement issue because it is not trivial.

@michalvavrik michalvavrik force-pushed the feature/ws-next-refresh-token-sub-protocol branch from bd4f6a3 to 0410ce0 Compare June 1, 2025 18:39

This comment has been minimized.

This comment has been minimized.

@michalvavrik michalvavrik force-pushed the feature/ws-next-refresh-token-sub-protocol branch from 0410ce0 to f5e5b28 Compare June 1, 2025 19:41

This comment has been minimized.

@michalvavrik michalvavrik requested a review from sberyozkin June 1, 2025 20:12

This comment has been minimized.

@sberyozkin
Copy link
Member

sberyozkin commented Jun 3, 2025

@michalvavrik Right, I did not mean to suggest it must be implemented similarly to the Quarkus OIDC authorization flow, I was implying the security characteristics of refreshing tokens were different.
With the Quarkus OIDC authorization flow, the token refresh and the identity update happens internally. Here, the refreshed token is forwarded to Quarkus via the public SPA.
I suppose it is the same security risk as with the actual upgrade where an initial token is submitted, but it needs a bit more thinking about.

@cescoffier
Copy link
Member

Isn’t this close to the legacy oauth2 flow, where the token is sent to the client/spa? There are some security risks there.

Also, what is required from the WebSocket client (especially in the browser)?

if (result && connectionOpened) {
console.log('Token updated, sending new token to the server')
socket.send(JSON.stringify({
metadata: {
Copy link
Member

Choose a reason for hiding this comment

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

This is creating a specific protocol. It should be a WS subprotocol, I guess.

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 proposed subprotocol, but it does enforce concrete data structure and we cannot do this to users. We don't know how binary or even text messages look like and it is up to customers how they send metadata between FE and BE. I think this is much more flexible. This example your are commenting on is an example. You can use a different DTO.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, but if there is a well-established sub-protocol that does what we need, we should use that instead. I’m checking right now, but I can not find anything obvious.

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.

I understand the idea, but we need to check if there is no existing WS subprotocol handling this.

@michalvavrik
Copy link
Member Author

michalvavrik commented Jun 3, 2025

Isn’t this close to the legacy oauth2 flow, where the token is sent to the client/spa?

If there is more than one backend microservice, than I don't believe users can use Quarkus OIDC web-app (authorization code flow on the Quarkus side) because they would have to authenticate with the every back-end service. So sending bearer access token must be pretty much standard? Now sending it with the WS is definitely less secure even over wss, I agree.

There are some security risks there.

I'd like to learn more about these security risks, can you say more, please? Apart of having it unintentionally in access logs etc. I honestly don't see a difference to the current situation, refresh token is stored in the memory, that variable is only accessible in the ESM module. I am sure that sending token via headers using JS client has security risks, I am just trying to understand the increased risk by sending refreshed token as a message.

Also, what is required from the WebSocket client (especially in the browser)?

Here is working example with JS WS client quarkusio/quarkus-quickstarts#1534. I can be more specific in my answer, but I am not sure if I know what specifically you are asking about. JS WS client sends a new access token as a message.

I understand the idea, but we need to check if there is no existing WS subprotocol handling this.

I didn't find any "de-facto" standard (or any FWIW). But I don't think it means there is none, I just googled it, not an expert 🤷‍♂️ .

Copy link
Contributor

@melloware melloware left a comment

Choose a reason for hiding this comment

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

I just ran into this. Looking forward to this

@michalvavrik michalvavrik force-pushed the feature/ws-next-refresh-token-sub-protocol branch from f5e5b28 to 8c13f0b Compare June 4, 2025 14:43

This comment has been minimized.

This comment has been minimized.

@michalvavrik michalvavrik requested a review from cescoffier June 18, 2025 21:15
@michalvavrik michalvavrik force-pushed the feature/ws-next-refresh-token-sub-protocol branch from 8c13f0b to dac2304 Compare June 22, 2025 15:17
@michalvavrik
Copy link
Member Author

michalvavrik commented Jun 22, 2025

I have just rebased on the current main as it has been a while, no changes. @sberyozkin @mkouba I think it is your turn :-)

Copy link

quarkus-bot bot commented Jun 22, 2025

Status for workflow Quarkus Documentation CI

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

✅ 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.

Warning

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

Copy link

quarkus-bot bot commented Jun 22, 2025

Status for workflow Quarkus CI

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

✅ 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 Windows

📦 extensions/opentelemetry/deployment

io.quarkus.opentelemetry.deployment.OpenTelemetryContinuousTestingTest.testContinuousTesting - History

  • io.quarkus.builder.BuildException: Build failure: Build failed due to errors [error]: Build step io.quarkus.devservices.deployment.DevServicesProcessor\#config threw an exception: java.lang.IllegalThreadStateException: process has not exited at java.base/java.lang.ProcessImpl.exitValue(ProcessImpl.java:566) at io.quarkus.deployment.util.ContainerRuntimeUtil.getVersionOutputFor(ContainerRuntimeUtil.java:239) at io.quarkus.deployment.util.ContainerRuntimeUtil.getContainerRuntimeEnvironment(ContainerRuntimeUtil.java:116) - java.lang.RuntimeException
java.lang.RuntimeException: 
io.quarkus.builder.BuildException: Build failure: Build failed due to errors
	[error]: Build step io.quarkus.devservices.deployment.DevServicesProcessor#config threw an exception: java.lang.IllegalThreadStateException: process has not exited
	at java.base/java.lang.ProcessImpl.exitValue(ProcessImpl.java:566)
	at io.quarkus.deployment.util.ContainerRuntimeUtil.getVersionOutputFor(ContainerRuntimeUtil.java:239)
	at io.quarkus.deployment.util.ContainerRuntimeUtil.getContainerRuntimeEnvironment(ContainerRuntimeUtil.java:116)
	at io.quarkus.deployment.util.ContainerRuntimeUtil.detectContainerRuntime(ContainerRuntimeUtil.java:66)
	at io.quarkus.deployment.util.ContainerRuntimeUtil.detectContainerRuntime(ContainerRuntimeUtil.java:55)

⚙️ JVM Integration Tests - JDK 17

📦 integration-tests/opentelemetry

io.quarkus.it.opentelemetry.LoggingResourceTest.testException - History

  • Condition with Lambda expression in io.quarkus.it.opentelemetry.LoggingResourceTest was not fulfilled within 2 minutes. - org.awaitility.core.ConditionTimeoutException
org.awaitility.core.ConditionTimeoutException: Condition with Lambda expression in io.quarkus.it.opentelemetry.LoggingResourceTest was not fulfilled within 2 minutes.
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
	at org.awaitility.core.CallableCondition.await(CallableCondition.java:78)
	at org.awaitility.core.CallableCondition.await(CallableCondition.java:26)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1160)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1129)
	at io.quarkus.it.opentelemetry.LoggingResourceTest.testException(LoggingResourceTest.java:113)
	at java.base/java.lang.reflect.Method.invoke(Method.java:569)

⚙️ JVM Integration Tests - JDK 21

📦 integration-tests/opentelemetry

io.quarkus.it.opentelemetry.LoggingResourceTest.testException - History

  • Condition with Lambda expression in io.quarkus.it.opentelemetry.LoggingResourceTest was not fulfilled within 2 minutes. - org.awaitility.core.ConditionTimeoutException
org.awaitility.core.ConditionTimeoutException: Condition with Lambda expression in io.quarkus.it.opentelemetry.LoggingResourceTest was not fulfilled within 2 minutes.
	at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:167)
	at org.awaitility.core.CallableCondition.await(CallableCondition.java:78)
	at org.awaitility.core.CallableCondition.await(CallableCondition.java:26)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1160)
	at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:1129)
	at io.quarkus.it.opentelemetry.LoggingResourceTest.testException(LoggingResourceTest.java:113)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

WebSocket-Next - Refresh OIDC AccessToken without reconnection
4 participants