softwaremill / akka-http-session

Web & mobile client-side akka-http sessions, with optional JWT support

Home Page:https://softwaremill.com/open-source/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

After invalidateSession I'm able to access secured endpoint with "invalidated" session

Fruzenshtein opened this issue · comments

commented

Hello

Looks like the invalidateSession function doesn't invalidate the session as it should. I'll explain below what I mean. If you want to look at steps to reproduce without technical details, scroll to the end :)

I use "com.softwaremill.akka-http-session" %% "core" % "0.4.0" with Scala version 2.12.1

I use following configs for session:

session {
  server-secret = "YzszrU1UkqsMqCNEnuLI8DDWs6Wqacj2z4dbtquSjB8GbsFpBA7GG38yk0DaIyrB"
  encrypt-data = true
  header {
    send-to-client-name = "Set-Authorization"
    get-from-client-name = "Authorization"
  }
}

Here is my session serialization (de-)

case class Session(role: String, email: String)
object Session {
implicit def serializer: SessionSerializer[Session, String] =
  new MultiValueSessionSerializer[Session](
    (session => Map(
      "role" -> session.role,
      "email" -> session.email)),
    (map => Try {
      Session(
        map.get("role").get,
        map.get("email").get)
    })
  )
}

And finally routes:

val routes = path("login") {
post {
  entity(as[Credentials]) { credentials =>
    onSuccess(userActor ? Authenticate(credentials)) {
      case loggedIn: LoggedIn => {
        setSession(oneOff, usingHeaders, Session(loggedIn.user.role, loggedIn.user.email)) {
          complete(HttpResponse(StatusCodes.OK))
        }
      }
      case noSuchEmail: NoUserWithEmail => complete(HttpResponse(StatusCodes.BadRequest))
      case InvalidPassword => complete(HttpResponse(StatusCodes.BadRequest))
    }
  }
}
} ~ path("me") {
get {
  requiredSession(oneOff, usingHeaders) { session =>
    complete(session.role)
  }
}
} ~ path("logout") {
post {
  requiredSession(oneOff, usingHeaders) { session =>
    invalidateSession(oneOff, usingHeaders) {
      complete(HttpResponse(StatusCodes.OK))
    }
  }
}
}

Here is what I do:

  1. Call POST /login and receive back in the header long_encrypted_token_A
  2. Call GET /me with the long_encrypted_token_A header and receive back appropriate response with ADMIN value
  3. Call POST /logout and receive back 200 response (here I assume that the session is invalidated)
  4. Call GET /me with the long_encrypted_token_A header and receive back appropriate response with ADMIN value

So the question:

Why I can still successfully can use the token after invalidation?

Thanks

If you are using the header transport, then invalidating the session responds with an empty Set-Authorization header, which is assumed to clear the client's storage. However that's of course up to your code to do that - probably it's worth documenting a bit better? (with cookies, this is done automatically by the browser).

Note that except for refreshable sessions, akka-http-session is stateless - it doesn't store the sessions anywhere, so the library itself has no way of knowing if the token was previously invalidated or not.

commented

@adamw thanks for the explanation.

So this happens due that fact that in akka-http-session a session is a stateless and once it is generated I can use it until its identifier is deleted locally on a client side (browser, mobile device)

Correct?

Yes. That's why sessions should always have an expiry date :) Optionally refreshed with the refresh token - which assumes external storage and "global" invalidation.

I'll keep this open to clarify the docs later :)

commented

@adamw deal :)

If you are using the header transport, then invalidating the session responds with an empty Set-Authorization header, which is assumed to clear the client's storage.

Hi all, got same problem regarding invalidation. I use InMemoryRefreshTokenStorage[T] and it stores session data well during app lifecycle. I use header transport as well and invalidating the session response with an empty Set-Authorization header, that's ok. But, regarding the sources

private[session] def invalidateRefreshableSession[T](sc: Refreshable[T], st: GetSessionTransport): Directive0 = {
    import sc.ec
    read(sc, st).flatMap {
      case None => pass
      case Some((v, setSt)) =>
        val deleteTokenOnClient = setSt match {
          case CookieST => deleteCookie(sc.refreshTokenManager.createCookie("").copy(maxAge = None))
          case HeaderST => respondWithHeader(sc.refreshTokenManager.createHeader(""))
        }

        deleteTokenOnClient &
          onSuccess(sc.refreshTokenManager.removeToken(v))
    }
  }

it should remove session from storage, especially with onSuccess(sc.refreshTokenManager.removeToken(v))
but it doesn't.

Could you clarify if everything should work as @adamw described, why this method is here and what for?

Please assume my code is similar to @Fruzenshtein implementation, expanded only by InMemoryRefreshTokenStorage[T], there is no something special.

Thank you for helping.

PS tested with scalatest and curl

@kormoglaz so you are saying that the token is not removed from storage? That should happen ... maybe you can try with a copy of InMemoryRefreshTokenStorage and with some debugging statements added.

Btw. this storage isn't mean for production, only for testing. It's not thread-safe (but making it such wouldn't be hard, just a different Map implementation)