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

io.quarkus.oidc.TokenStateManager called from event loop thread #19670

Closed
danelowe opened this issue Aug 26, 2021 · 10 comments · Fixed by #19807
Closed

io.quarkus.oidc.TokenStateManager called from event loop thread #19670

danelowe opened this issue Aug 26, 2021 · 10 comments · Fixed by #19807
Assignees
Labels
Milestone

Comments

@danelowe
Copy link

Describe the bug

I'm trying to implement a custom io.quarkus.oidc.TokenStateManager that stores some tokens in Redis.

I get java.lang.IllegalStateException: The current thread cannot be blocked: vert.x-eventloop-thread-7

In CodeAuthenticationMechanism:

    private Uni<SecurityIdentity> reAuthenticate(Cookie sessionCookie,
            RoutingContext context,
            IdentityProviderManager identityProviderManager,
            TenantConfigContext configContext) {

        AuthorizationCodeTokens session = resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig,
                sessionCookie.getValue());

        context.put(OidcConstants.ACCESS_TOKEN_VALUE, session.getAccessToken());
...

Expected behavior

Either call TokenStateManager methods from another thread, or allow for a reactive API in the TokenStateManager interface such that a database can be called without throwing an error.

Or document how it is to be achieved.

Actual behavior

I get java.lang.IllegalStateException: The current thread cannot be blocked: vert.x-eventloop-thread-7 when calling redis to retrieve tokens.

How to Reproduce?

Try something like:

import io.quarkus.oidc.AuthorizationCodeTokens
import io.quarkus.oidc.OidcTenantConfig
import io.quarkus.oidc.TokenStateManager
import io.quarkus.redis.client.RedisClient
import io.quarkus.redis.client.reactive.ReactiveRedisClient
import io.quarkus.security.AuthenticationFailedException
import io.smallrye.common.annotation.Blocking
import io.vertx.ext.web.RoutingContext
import java.util.*
import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject

@ApplicationScoped
class TestTokenStateManager : TokenStateManager {

    @Inject
    lateinit var redisClient: RedisClient

    @Inject
    lateinit var reactiveRedisClient: ReactiveRedisClient

    override fun createTokenState(
        routingContext: RoutingContext,
        oidcConfig: OidcTenantConfig,
        tokens: AuthorizationCodeTokens
    ): String {
        val id = UUID.randomUUID().toString()
        val maxAge: Long = routingContext.get(SESSION_MAX_AGE_PARAM)
        val value = listOf(tokens.idToken, tokens.accessToken, tokens.refreshToken).joinToString(STORAGE_DELIMITER)
        reactiveRedisClient.setex(getRedisKey(id), (maxAge * 3).toString(), value)
        return id
    }

    @Blocking
    override fun getTokens(
        routingContext: RoutingContext,
        oidcConfig: OidcTenantConfig,
        tokenState: String
    ): AuthorizationCodeTokens {
        return if (isWebApp(routingContext, oidcConfig)) {
            if (!isValidUUID(tokenState)) {
                throw AuthenticationFailedException("Session ID is not valid UUID")
            }
            val (idToken, accessToken, refreshToken) = redisClient.get(getRedisKey(tokenState)).toString()
                .split(STORAGE_DELIMITER)
            AuthorizationCodeTokens(idToken, accessToken, refreshToken)
        } else {
            AuthorizationCodeTokens(tokenState, null, null)
        }
    }

    override fun deleteTokens(routingContext: RoutingContext, oidcConfig: OidcTenantConfig, tokenState: String) {
        if (isWebApp(routingContext, oidcConfig)) {
            reactiveRedisClient.del(listOf(getRedisKey(tokenState)))
        }
    }

    private fun getRedisKey(id: String): String {
        return "sess:$id"
    }

    private fun isValidUUID(uuid: String) = UUID.fromString(uuid).toString() == uuid

    private fun isWebApp(context: RoutingContext, oidcConfig: OidcTenantConfig): Boolean {
        return if (OidcTenantConfig.ApplicationType.HYBRID == oidcConfig.applicationType) {
            context.request().getHeader("Authorization") == null
        } else OidcTenantConfig.ApplicationType.WEB_APP == oidcConfig.applicationType
    }

    companion object {
        const val SESSION_MAX_AGE_PARAM = "session-max-age"
        const val STORAGE_DELIMITER = "|"
    }
}

Output of uname -a or ver

Darwin Danes-MBP 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64

Output of java -version

openjdk version "15.0.2" 2021-01-19 OpenJDK Runtime Environment Corretto-15.0.2.7.1 (build 15.0.2+7) OpenJDK 64-Bit Server VM Corretto-15.0.2.7.1 (build 15.0.2+7, mixed mode, sharing)

GraalVM version (if different from Java)

No response

Quarkus version or git rev

2.1.0.Final

Build tool (ie. output of mvnw --version or gradlew --version)

------------------------------------------------------------ Gradle 7.0.2 ------------------------------------------------------------ Build time: 2021-05-14 12:02:31 UTC Revision: 1ef1b260d39daacbf9357f9d8594a8a743e2152e Kotlin: 1.4.31 Groovy: 3.0.7 Ant: Apache Ant(TM) version 1.10.9 compiled on September 27 2020 JVM: 15.0.2 (Amazon.com Inc. 15.0.2+7) OS: Mac OS X 11.4 x86_64

Additional information

No response

@danelowe danelowe added the kind/bug Something isn't working label Aug 26, 2021
@quarkus-bot
Copy link

quarkus-bot bot commented Aug 26, 2021

/cc @evanchooly, @pedroigor, @sberyozkin

@danelowe
Copy link
Author

I think this might make #13239 difficult?

@stuartwdouglas
Copy link
Member

@sberyozkin can you do the same thing here I did for the TenantConfigResolver?

@sberyozkin
Copy link
Member

@stuartwdouglas Yeah, I was thinking I should try and copy that code you added and rename a bit :-), I'll give it a go

@sberyozkin sberyozkin self-assigned this Aug 26, 2021
@sberyozkin
Copy link
Member

I've started prototyping it, though may have to delay for a few days or so

@sberyozkin
Copy link
Member

@danelowe Hi, please consider looking at #13239 when you get a chance - as you are experimenting with TokenStateManager storing the data in a cache... By the way, re your code fragment - you can assume it is always a web-app even if it is configured as hybrid - as only CodeAuthenticationMechanism calls TokenStateManager

@gsmet gsmet modified the milestones: 2.3 - main, 2.2.2.Final Sep 6, 2021
@danelowe
Copy link
Author

danelowe commented Sep 7, 2021

Thanks for working on this @sberyozkin. I've shifted to other work for now. By the time I get back to this, 2.3 might be released. When I get back to it, I will have a look at how I can make a generic solution.

@t-adrian
Copy link

t-adrian commented Sep 23, 2021

G'day @sberyozkin @stuartwdouglas, know this issue is closed but was wondering if there are any tips on the pattern to perform a blocking call in the TokenStateManager like saving a PanacheEntity?

@t-adrian
Copy link

G'day @sberyozkin @stuartwdouglas, know this issue is closed but was wondering if there are any tips on the pattern to perform a blocking call in the TokenStateManager like saving a PanacheEntity?

Replying to my own question, the answer is to use requestContext.runBlocking(()-> do blocking logic here) and annotate the method with @Blocking.

@sberyozkin
Copy link
Member

@t-adrian thanks, I missed it, good you've figured it out

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

Successfully merging a pull request may close this issue.

5 participants