DEV Community

Cover image for GraphQL backend — token expiry

Posted on • Originally published at

GraphQL backend — token expiry

In the previous post we covered authentication and role management. However, it assumed any invalid session to be a visitor instead of throwing 401—Unauthorized client error. This post covers the token expiry aspect of API call.

Let's start with coding part. Following block diagram explains the interaction between each component. As usual, we'll build the system bottom-up.

🎟 Token Service

To manage session and expire it, we need meta attributes other than just the token. So, let's beef up the token to a data class called SessionToken. This will hold created & last access timestamps in addition to the token itself.

data class SessionToken(
    val token: String,
    val created: OffsetDateTime,
    var lastAccess: OffsetDateTime = created

Enter fullscreen mode Exit fullscreen mode

Our token service is a simple repository which abstracts CRUD on token. When user login-register, a new token generated and wrapped in SessionToken. For every web request made, the token updated with last access time. In case user logout or token expired, entry will be deleted.

Below is our simple implementation — we store our mock tokens as usual to keep our focus on expiry. The created time points to now() i.e we emulate like user logged in at the time of launch.

class TokenService {

    private val store = mutableMapOf<String, SessionToken>()

    init {
        store["token-buyer"] =
        store["token-seller"] =

    fun createToken() = SessionToken(
        token = UUID.randomUUID().toString(),
        created =

    fun peekToken(token: String?): SessionToken? = store[token]

    fun updateLastAccessTime(token: String) {
        // TODO: Token not found in store - fishy!!
        store[token]?.lastAccess =

    fun deleteToken(token: String?) {

Enter fullscreen mode Exit fullscreen mode

⏲ Token Expiry Strategy

When it comes to expiry, it's always a good practice to abstract out the validation part and create functional interface to carry out the validation. This will help us plug-in different implementations in the system with little config change.

This brings us to a functional interface SessionExpiryStrategy which takes a SessionToken validate and return whether it is expired.

interface SessionExpiryStrategy {

    fun isSessionExpired(token: SessionToken): Boolean


Enter fullscreen mode Exit fullscreen mode

An implementation is only good when explained with its implementations. We consider three different strategies for this.

  1. Fixed lifespan : Session will expire after 30 days
  2. Timeout : If user is not active for 3 minutes — logout
  3. Extended Lifespan: If user is active during the 30th day, give him grace period in 3 minute window
class FixedLifeSpanStrategy : SessionExpiryStrategy {
    // 30 days fixed lifespan
    val MAX_DURATION = 30 * 24 * 60 * 60
    override fun isSessionExpired(token: SessionToken): Boolean {
        val durationSinceLogin = ChronoUnit.SECONDS.between(
        return durationSinceLogin > MAX_DURATION
Enter fullscreen mode Exit fullscreen mode
class TimeoutExpiryStrategy : SessionExpiryStrategy {
    // Logout after 3 minutes of inactivity
    const val MAX_DURATION = 3 * 60
    override fun isSessionExpired(token: SessionToken): Boolean {
        val durationSinceLastAccess = ChronoUnit.SECONDS.between(
        return durationSinceLastAccess > MAX_DURATION
Enter fullscreen mode Exit fullscreen mode
class ExtendedLifeSpanStrategy : SessionExpiryStrategy {
    private val timeoutExpiryStrategy = TimeoutExpiryStrategy()
    private val fixedLifeSpanStrategy = FixedLifeSpanStrategy()
    override fun isSessionExpired(token: SessionToken): Boolean {
        return if (fixedLifeSpanStrategy.isSessionExpired(token)) {
            // 30 days past, if user is in the mid of something try the timeout
            // to extend his session
        } else {
            // 30 days not past yet Let him use the system
Enter fullscreen mode Exit fullscreen mode

Few Usecases:
As you can see in ExtendedLifeSpanStrategy, we can mix-match the implementations to tailor user experience. Let's say the buyer in our e-commerce website actively purchasing something and just before he makes payment, session expire due to 30 day lifespan — It would be frustrating. Three minutes added on each api call will alleviate the expiry.

If we want Seller account to logout on 3 minutes inactivity and a fixed timespan for Buyer, it can be achieved by picking strategy 'per role'.

val strategies = mapOf(
    BUYER to fixedTimeSpan,
    SELLER to timeoutStrategy



Enter fullscreen mode Exit fullscreen mode

We have a strategy and a tokenservice in place — let's move on to the final piece where we connect both.

👮 Request Manager

Before we move on to the logic, let's fix the 🐞 in user service. From now, it will return null role for unknown/expired tokens. Still, if there is no token present, session will be identified as VISITOR.

    fun identifyRole(token: String?): Roles? {
        return if (token == null)

Enter fullscreen mode Exit fullscreen mode

In the request manager, wire the expiry strategy as you see fit. We'll use qualifiers to inject different strategies that we implemented in last section.

//    @Qualifier("extended")
//    @Qualifier("fixed_time")
    private lateinit var tokenExpiryStrategy: SessionExpiryStrategy

     // Save the session info per request. Retrieve it throughout the request
    fun saveSession(request: HttpServletRequest) {
        // Retrieve auth token from request - if any
        val token: String? = request.getHeader(HEADER_TOKEN)

        // Identify role
        val role = userService.identifyRole(token)
        if (role == null) {
            throw unauthorizedException()

        // Handling users that need a session
        if (role != Roles.VISITOR) {
            val sessionToken = tokenService.peekToken(token)
            if (sessionToken == null || tokenExpiryStrategy.isSessionExpired(sessionToken)) {
                throw unauthorizedException()

            // punch-in to the token service

            KEY_SESSION, DummySession(
                role = role

Enter fullscreen mode Exit fullscreen mode

Added inline comments for clarity, again a walkthrough here. If role is null, throw exception. If session token is null or expired for non-VISITORs throw exception. In both cases wipe the token off the system.

So, what do we throw? — 401 response.

   private fun unauthorizedException() = HttpClientErrorException.create(
Enter fullscreen mode Exit fullscreen mode

If the session is valid, update the last access time in token service to support TimeoutStrategy.

🐞 More on error handling

In case, the session is timeout, don't let the request proceed further to protect resources. i.e break the chain in request filter.

class DummyRequestFilter : OncePerRequestFilter() {

    private lateinit var requestManager: DummyRequestManager

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        try {
            // Feed the request to request manager for session preparation

            // Resume with request
            filterChain.doFilter(request, response)
        } catch (e: HttpClientErrorException) {
            response.sendError(e.rawStatusCode, e.statusText)

Enter fullscreen mode Exit fullscreen mode

In dummy request filter, once per request check whether the token is valid. If so, resume the chain, otherwise catch and propogate the 401 error code to the client.

🐛 That's it 🐛

🚀 Run it!

curl 'http://localhost:8080/graphql' \
  -H 'x-auth-token: token-random-one' \
  -H 'Content-Type: application/json' \
  --data-raw '{"query":"mutation {addProduct}","variables":null}' \


curl 'http://localhost:8080/graphql' \
  -H 'x-auth-token: token-seller' \ 
  -H 'Content-Type: application/json' \
  --data-raw '{"query":"mutation {addProduct}","variables":null}' \


Enter fullscreen mode Exit fullscreen mode

📖 Resources

  1. Github repo
  2. GraphQL playground — http://localhost:8080/graphiql

Top comments (0)