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

Spring Session / Security - Redis Sessions not being properly deleted #3177

Open
dreamstar-enterprises opened this issue Aug 25, 2024 · 0 comments
Labels
status: waiting-for-triage An issue we've not yet triaged type: bug A general bug

Comments

@dreamstar-enterprises
Copy link

Hi,

I have a Spring OAuth Client (BFF), between a Public Angular Client, and an Auth0 Authorization server. When I login, the BFF correctly persists the session to Redis (and with it, the Authorized Client, Security Context, and Authorized Request as attributes in the session)

When I logout though, only the contents of the Session get deleted, the key itself does not. Also nothing in the Sorted Set, ever gets deleted. I am positing here, as it might be a genuine bug.

Logout Handler

Here is my Logout Handler

@Component
internal class SessionServerLogoutHandler(
    private val sessionControl: SessionControl,
    private val sessionProperties: SessionProperties,
    private val csrfProperties: CsrfProperties,
) : ServerLogoutHandler {

    private val logger = LoggerFactory.getLogger(SessionServerLogoutHandler::class.java)

    override fun logout(exchange: WebFilterExchange, authentication: Authentication?): Mono<Void> {
        return exchange.exchange.session
            .flatMap { session ->
                logger.info("Logging out: Invalidating User Session: ${session.id}")

                val response = exchange.exchange.response
                sessionControl.invalidateSession(session)
                    .then(Mono.fromRunnable {

                        logger.info("Deleting Session Cookie: ${sessionProperties.SESSION_COOKIE_NAME}")

                        // delete the session cookie
                        val sessionCookie = ResponseCookie.from(sessionProperties.SESSION_COOKIE_NAME)
                        sessionCookie.maxAge(0)
                        sessionCookie.httpOnly(sessionProperties.SESSION_COOKIE_HTTP_ONLY)
                        sessionCookie.secure(sessionProperties.SESSION_COOKIE_SECURE)
                        sessionCookie.sameSite(sessionProperties.SESSION_COOKIE_SAME_SITE)
                        sessionCookie.path(sessionProperties.SESSION_COOKIE_PATH)
                        sessionCookie.domain(sessionProperties.SESSION_COOKIE_DOMAIN)
                        .build()
                        response.headers.add(
                            HttpHeaders.SET_COOKIE,
                            sessionCookie.toString()
                        )

                        logger.info("Deleting Session Cookie: ${csrfProperties.CSRF_COOKIE_NAME}")

                        // delete the CSRF cookie
                        val csrfCookie = ResponseCookie.from(csrfProperties.CSRF_COOKIE_NAME)
                        csrfCookie.maxAge(0)
                        csrfCookie.httpOnly(csrfProperties.CSRF_COOKIE_HTTP_ONLY)
                        csrfCookie.secure(csrfProperties.CSRF_COOKIE_SECURE)
                        csrfCookie.sameSite(csrfProperties.CSRF_COOKIE_SAME_SITE)
                        csrfCookie.path(csrfProperties.CSRF_COOKIE_PATH)
                        csrfCookie.domain(csrfProperties.CSRF_COOKIE_DOMAIN)
                        .build()
                        response.headers.add(
                            HttpHeaders.SET_COOKIE,
                            csrfCookie.toString()
                        )
                    })
            }
    }
}

SessionControl

It calls another call called SessionControl, and the invalidate session method. Here is that function

fun invalidateSession(session: WebSession): Mono<Void> {
        val sessionInformation = getSessionInformation(session)
        logger.info("Invalidating sessionId: ${sessionInformation.sessionId}")
        // handle the session invalidation process
        return sessionInformation.invalidate()
            .then(Mono.defer {
                webSessionStore.removeSession(sessionInformation.sessionId)
            })
            .doOnSuccess {
                logger.info("Session invalidated and removed: ${sessionInformation.sessionId}")
            }
            .doOnError { error ->
                logger.error("Error invalidating session: ${sessionInformation.sessionId}", error)
            }
    }

WebSessionStore

That inturn calls Websession Store, and it's removeSession method.

Here is the bean and the function:

@Bean(name = ["webSessionStore"])
fun webSessionStore(
    reactiveRedisIndexedSessionRepository: ReactiveRedisIndexedSessionRepository
): SpringSessionWebSessionStore<RedisSession> {
    return SpringSessionWebSessionStore(reactiveRedisIndexedSessionRepository)
}

@Override
public Mono<Void> removeSession(String sessionId) {
	return this.sessions.deleteById(sessionId);
}

reactiveRedisIndexedSessionRepository

The this.sessions above refers to the ReactiveRedisIndexedSessionRepository that was passed in the constructor of the WebsessionStore. Looking at the internals of the Spring ReactiveRedisIndexedSessionRepository I see this:

public Mono<Void> deleteById(String id) {
		return deleteAndReturn(id).then();
	}

	private Mono<RedisSession> deleteAndReturn(String id) {
		// @formatter:off
		return getSession(id, true)
				.flatMap((session) -> this.sessionRedisOperations.delete(getExpiredKey(session.getId()))
						.thenReturn(session))
				.flatMap((session) -> this.sessionRedisOperations.delete(getSessionKey(session.getId())).thenReturn(session))
				.flatMap((session) -> this.indexer.delete(session.getId()).thenReturn(session))
				.flatMap((session) -> this.expirationStore.remove(session.getId()).thenReturn(session));
		// @formatter:on
	}

Session in Redis before

As you can see before I logout, the session is there in Redis.

enter image description here

Session in Redis after

After I call the logout handler, something has definitely happened, but the session is still there with its key, just no map of values apart from a single lastaccessed map key / value.

enter image description here

Further more nothing ever gets deleted from the SortedSet, which according to the 4th step in the deleteAndReturn method above, it should...

enter image description here

So, can someone help me understand where I may have gone wrong in my code?

@dreamstar-enterprises dreamstar-enterprises added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Aug 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: waiting-for-triage An issue we've not yet triaged type: bug A general bug
Projects
None yet
Development

No branches or pull requests

1 participant