Navigating Session Logouts, Timeouts, and Token Expiry

Introduction

The mechanics of managing session logout, session timeout, and token expiry are crucial in creating a secure and seamless online user experience. Let's take a comprehensive look at how these aspects are handled in different contexts, specifically highlighting the approach employed by ZITADEL within the OpenID Connect (OIDC) paradigm.

Session Logout vs. Session Timeout

Let’s dive into the difference between a session expiring and a user explicitly logging out.

While both session logout and session expiration pertain to user sessions, they are two distinct mechanisms that serve different purposes. Here's a breakdown of the differences:

Session Logout Logout is an explicit action taken by the user to end a session, or it could also be an admin or a machine user who terminates a user session. By selecting "logout" or a similar option, the user signals to the system that they are done with their session, and the system should invalidate it. By logging out, users can ensure that their session is closed, preventing others from accessing their account or data, especially on shared or public devices.

Session Timeout Session expiration, often referred to as a timeout, encompasses two main concepts: inactivity and lifetime. Inactivity refers to a period during which a user doesn't perform any actions, leading to the session's termination after a predetermined duration. On the other hand, session lifetime is the maximum allowable duration a session can remain active, regardless of user activity.

Both mechanisms ensure that a user's session doesn't stay active indefinitely, especially in scenarios where the user might forget to log out. For instance, a web application might have an inactivity timeout of 30 minutes. If a user doesn’t interact with the system for this duration, the session will expire automatically. However, even if the user remains active, the session could still terminate upon reaching its maximum lifetime.

By implementing both inactivity-based expirations and maximum session lifetimes, systems can optimize resource utilization and strengthen security. This dual approach reduces potential vulnerabilities, especially in instances where a device is left unattended without a proper log-out.

Where Sessions are Stored and its Implications

In web applications, how sessions are stored is pivotal in shaping both the security landscape and the user experience. Two methods of session storage are user-agent-based and server-based storage, each with its unique pros and cons.

User-Agent-Based Storage

This approach stores session information directly within the user agent, typically a browser or mobile app, often via cookies.

Benefits:

  • Speed and Efficiency: Since the data is stored on the client side, there's no need for a round trip to the server to retrieve session information, leading to faster response times.
  • Scalability: Reduces server load, as the server doesn't have to maintain and manage active sessions, making it easier to scale applications.

Drawbacks:

  • Security Vulnerability: Malicious actors can potentially access and manipulate session data stored in the user agent, especially if the data isn't well protected.
  • Data Persistence: Depending on the user's settings, session data might be cleared upon closing the browser, affecting the continuity of the user's session.
  • Size Limitations: Browsers have a limit to the size of cookies, constraining the amount of session data that can be stored.
  • Revocation Complexity: Revoking or invalidating session data stored in the user agent is more complex. Implementing a server-side blacklist might be necessary to invalidate specific sessions, but this approach isn't recommended due to scalability and security concerns.

Server-Based Storage

With this method, session data is stored on the server, for instance, in a database or API.

Benefits:

  • Enhanced Security: Storing session data server-side means it's protected behind server security layers, reducing the risk of unauthorized access.
  • Data Consistency: Session data remains consistent and isn't dependent on individual user-agent behavior or settings.
  • Greater Storage Capacity: Unlike client-side storage with size constraints, server-side allows for more extensive data storage.

Drawbacks:

  • Latency: Retrieving session data requires communication between the server and client, potentially increasing load times.
  • Server Load: Storing and managing sessions server-side can strain server resources, especially with many concurrent users.

ZITADEL, for instance, generates a user-agent cookie once a user authenticates. While this provides a unique identifier for the user-agent, the session data remains safely on the server. However, if the user agent's cookie is deleted, it disrupts the connection to the session data that is stored server-side, emphasizing the importance of maintaining this identifier.

In sum, the choice between user-agent-based and server-based storage hinges on the specific needs and priorities of the application, whether it's speed, scalability, security, or data integrity.

User Logout

OpenID Connect and its Logout Mechanisms

OpenID Connect (OIDC) is a modern authentication protocol extending the OAuth 2.0 framework, facilitating secure user identity assertion. Primarily used for single sign-on (SSO) processes, OIDC allows applications to verify the identity of users based on the authentication performed by an authorization server, thus streamlining user access across multiple applications.

Complementing its authentication capabilities, OIDC introduces comprehensive logout mechanisms to ensure sessions are securely terminated across all linked applications:

  1. Session Management 1.0: This mechanism enables clients (or Relying Parties) to monitor the state of a user's session at the identity provider (e.g., ZITADEL). By doing so, a user's logout from the provider can be detected, allowing the client to reflect this change promptly.
  • Use Cases:
    • In web applications or services, it's essential to monitor the user's session state continuously.
    • When there's a need for the client (Relying Party) to react to changes in the user's session state at the identity provider, such as logging out.

ZITADEL supports this mechanism. When a user logs out from ZITADEL, the session is terminated, and the client can adjust its behavior accordingly using the RP-initiated flow.

  1. Front-Channel Logout 1.0: Implemented within the user agent, this approach facilitates simultaneous logout requests across all clients sharing the user's OpenID session. Each participating client uses an iframe, ensuring that the logout process is executed in parallel for all.
  • Use Cases:
    • In situations where it's beneficial to execute logout requests simultaneously across all clients sharing the user's OpenID session. This ensures a coordinated logout from all apps without relying on back-end communication.
    • Especially useful in architectures where the client can render an iframe, which aids in performing logout requests in parallel.
  1. Back-Channel Logout 1.0: Operated server-side, this mechanism ensures that a user can be logged out from all clients, even if the user agent is closed or disconnected. It provides a robust way to ensure all sessions are terminated, regardless of the state of individual client applications.
  • Use Cases:
    • For systems where server-side operations are prioritized, ensure users are logged out from all connected applications even if the user agent (e.g., a web browser) is closed.
    • In scenarios where it's crucial to maintain the integrity of session termination across various apps, regardless of the state or behavior of individual client apps.

RP-Initiated Logout

When the Relying Party wants to log out a user, it can initiate the process. The Relying Party initiates this flow by making a request to the identity provider's end_session_endpoint. This is especially important in Single Sign-On scenarios. If a user logs out of one application, you might want them to be logged out of all other applications they accessed with the same credentials.

ZITADEL, adhering to OpenID Connect standards, provides the end_session_endpoint for session terminations. This endpoint is the interface through which client applications can request the conclusion of a user's session. Here's how this typically works:

  1. Initiation: The RP sends a request to the end_session_endpoint provided by ZITADEL. This request typically includes an id_token_hint, a previously issued ID Token, and a post_logout_redirect_uri, which must match one of the pre-registered post-logout redirect URIs in ZITADEL for the RP).
  2. Processing: ZITADEL processes this request, invalidates the user's session, and ensures the user is logged out.
  3. Redirect: Optionally, after the logout has been performed, ZITADEL can redirect the user-agent back to the RP, using the provided post_logout_redirect_uri. This allows the application to provide feedback to the user or take additional post-logout actions.

Using Python, an RP-initiated logout might look like this:

import requests
from urllib.parse import urlencode

''' Configuration'''
ZITADEL_DOMAIN = "https://your_zitadel_domain"
END_SESSION_ENDPOINT = "/oidc/v1/end_session"
POST_LOGOUT_REDIRECT_URI = "https://your_app_domain/logged_out"
ID_TOKEN = "your_id_token"  ''' You should obtain this after user authentication'''
STATE = "random_string"  ''' This can be any random unique string to prevent CSRF '''

def logout_using_zitadel():
    params = {
        'id_token_hint': ID_TOKEN,
        'post_logout_redirect_uri': POST_LOGOUT_REDIRECT_URI,
        'state': STATE
    }
    logout_url = ZITADEL_DOMAIN + END_SESSION_ENDPOINT + '?' + urlencode(params)
    
    response = requests.get(logout_url)
    
    if response.status_code == 200:
        print("Logout successful!")
    else:
        print(f"Logout failed with status code {response.status_code}. Response: {response.text}")

if __name__ == "__main__":
    logout_using_zitadel()

Make sure to fill in the placeholders (your_zitadel_domain, your_app_domain, and your_id_token) with your actual values. This is a simplified example for illustration and doesn't include error handling, logging, or advanced features like session management. You'd typically incorporate such a function in a larger application, possibly in conjunction with a web framework like Flask or Django.

In Context with OpenID Connect's Logout Mechanisms:

The RP-initiated Logout can be seen as a sub-component of OpenID Connect's Session Management 1.0 mechanism. While Session Management provides the tools to monitor the session's state, RP-initiated Logout allows an active role in ending sessions when needed. This synergy ensures that applications not only passively observe the session state but also have the tools to act when user sessions need to be concluded.

Session Expiry and Token Management: Balancing Security and Usability

The notion of session timeout (or logout) in the context of OIDC is not just about when the user's web session expires but also about the lifespan of tokens used in the authentication and authorization processes.

With the rise of stateless applications and RESTful APIs, OAuth/OIDC introduced Bearer tokens, specifically Access Tokens (Opaque or JWT), that clients store and send with requests to access resources. These tokens are stateless by design, meaning the server doesn’t need to maintain session data between requests. This approach increases scalability but poses challenges for traditional server-side activity tracking. Instead, the task of tracking activity often shifts to the client, and token expiration mechanisms play a pivotal role in managing inactivity. Tokens are validated against introspection endpoints to determine if they are still active.

An expired token doesn't always mean an ended session. A token can expire, but as long as the session is active, the user can obtain a new token. Conversely, if a session ends, tokens associated with that session should be considered invalid, even if their expiry time hasn't been reached.

The Token Rotation Approach

With token rotation, instead of providing the client with a single long-lasting token, the client is given an access token with a brief lifespan (e.g., 15 minutes) and a refresh token that can be used to obtain new access tokens when they expire. This ensures that even if a token is compromised, its window of vulnerability is limited. Moreover, if a user becomes inactive and closes their client or if the client fails to refresh the token for some reason, the access token expires rapidly, effectively emulating the behavior of a session timeout.

Implications of Refresh Token Rotation:

  • Enhanced Security: If a refresh token gets compromised, it can only be used once. Any attempt to use it again (by either the legitimate client or a malevolent actor) will be flagged, making token thefts detectable.

  • Statefulness Concern: While refresh tokens in rotation enhance security, they introduce a level of statefulness to the process. Clients must ensure they replace the old refresh token with the new one seamlessly. Any desynchronization can lead to failed token refresh attempts.

See how to request a refresh token in ZITADEL here.

JWT vs. Opaque Tokens: Expiry and Validation

Opaque tokens are just random strings, often stored in a database with associated metadata, while JWTs contain a payload with information about the user and token attributes. Both token types can have expiration times, but the expiry of a JWT is embedded within the token itself. For opaque tokens, expiry information is usually stored on the server side. JWTs are self-contained and can be validated without a database lookup, making them efficient for distributed systems. However, they can be vulnerable to key rotation issues and potential token leakage. Opaque tokens, on the other hand, provide an extra layer of indirection and security as they require a database lookup for validation but might add some overhead to the validation process.

One of the essential claims in a JWT payload is the exp claim, which indicates the expiration time of the token. Systems validate the JWT by checking this claim against the current time. If the token has expired, the system can deny access, prompting the user to re-authenticate or use a refresh token if available.

JSON Web Key (JWK) Rotation

Users can call the /oauth/v2/keys endpoint of their OIDC server to retrieve the JSON Web Keys (JWKs), the cryptographic keys used to sign and validate JWTs. Once obtained, backend services can independently validate the token's signature. The process has two primary steps:

  1. Check the Cache: Before making external requests, the system should check its cache for the key corresponding to the JWT. If the key exists, it should be used for validation.
  2. Fetching Fresh Keysets: If the cache doesn't have the necessary key (determined using the key ID or kid), the system should fetch a fresh set of keys.

Key rotation refers to the practice of changing the JWKs. Regularly rotating these keys is a crucial security measure, ensuring that even if a key is compromised, it won't be valid for long. Key rotation can lead to situations where a valid JWT token (i.e., one that hasn't expired according to its exp claim) can no longer be verified because the key it was signed with is no longer in use. This is particularly an issue in systems where JWT lifetimes are long and keys are rotated frequently.

Strategies for JWK Rotation

To handle the above challenge, systems can:

  • Reduce the JWT lifetime, ensuring they expire shortly after key rotation.
  • Maintain a list of recently retired keys to validate tokens signed with them, but only for a limited grace period.
  • Promote the use of refresh tokens, which can obtain new JWTs signed with the latest key.

ZITADEL’s Token Handling Mechanism

The industry widely accepts that shorter timeouts increase security but might slightly hinder the user experience. Striking a balance is key.

Generally, best practices dictate that tokens should be securely stored, frequently validated, and inaccessible to potential attackers. ZITADEL chooses not to store the signed token directly but associates it with a session. ZITADEL leverages CockroachDB for global replication, and its event-sourcing model ensures robust and efficient token validation.

Configure Token Lifetime and Expiration

Diagram 1

Figure 1 - Configure token lifetime and expiration in ZITADEL UI

In addition to setting the duration of the lifetime of the access and ID tokens, a user can also configure the following parameters in their ZITADEL instance:

  • Refresh Token Idle Expiration: This represents the timeframe in which the refresh token must be used before it becomes idle. If a refresh token isn’t used within this timeframe, it becomes invalid, effectively logging the user out. This mechanism is akin to the traditional session timeout due to inactivity.

  • Refresh Token Expiration: This parameter defines the absolute lifespan of a refresh token. Regardless of how frequently it’s used to obtain new access tokens, once this period lapses, the refresh token expires. The user will then need to undergo a fresh authentication process to obtain a new set of tokens. This setting ensures that, even with regular activity, users re-authenticate periodically, further bolstering security.

Note that while the ZITADEL UI might present durations in days (although the UI does allow fractional inputs such as 0.1 days), the API uses a "Duration" type, which is likely more granular and flexible. Users interfacing directly with the API need to be aware of the expected format and possible values to set durations correctly. Token lifetimes, rotation policies, and other behaviors are often configurable via the API. This allows developers to tailor the authentication and authorization flow to their application's unique needs, balancing user experience with security considerations.

Most systems, for enhanced security, invalidate refresh tokens upon explicit logout. ZITADEL, however, keeps refresh tokens active post-logout, a decision driven by usability considerations.

Read more about configuring token lifetime and expiration here. See the current API that handles refresh tokens here.

Managing JSON Web Keys (JWKs)

ZITADEL practices frequent key rotation to enhance security with keys having a predefined maximum lifetime of 6 hours, after which they are rotated. Each of these key pairs is associated with a unique kid. ZITADEL doesn't provide individualized configurations for token lifetimes on specific instances, which requires users to adapt to the platform's default configurations or consider self-hosting the solution for complete control over JWT settings.

User Experience and Reauthentication in ZITADEL

ZITADEL ensures that while security is paramount, users face minimal friction during their interactions. Let’s delve into some of these UX considerations.

  • Persistent Sessions: After a user logs into an application integrated with ZITADEL, subsequent logins can lead the user to a "Select your account" page. This feature reduces the need to input credentials continuously, speeding up re-authentication.

  • Logout Variability: Logging out doesn't always mean the same thing. A user may log out of an individual app, but if the centralized session with ZITADEL is still active, they might not need to re-enter their credentials for other connected apps.

  • Forced Reauthentication: By using the prompt: login parameter, developers can force users to manually input their credentials again, even if they have an active session. This can be essential for sensitive operations or applications.

  • Silent Authentication: Conversely, the prompt: none parameter ensures the user isn’t presented with any UI. This is handy for background operations or when a smooth user experience is crucial, and interruptions are undesired.

  • New Sessions API: The Sessions API (which is in a public preview state at the time of this writing) offers more granular control over user sessions. Using this, specific sessions can be terminated or users can be prompted to decide which session they wish to terminate.

  • Global Logout: Features like the "Log out all users" button in the ZITADEL UI offer administrators the power to globally end sessions, a useful tool during potential breach scenarios or maintenance periods.

Diagram 2

Figure 2 - Log out all users via the ZITADEL UI

Conclusion

In diving deep into session and token management, we've seen that achieving the right balance between security and usability is intricate. ZITADEL's methodical, standards-driven solution provides a robust template for those looking to master the complexities of session logout, timeout, and token expiry in their systems.

By default, ZITADEL leans towards using opaque tokens, prioritizing security, and then adjusting based on specific requirements, such as when performance demands the use of JWT. Even when employing JWT tokens, developers have the flexibility to call the introspect/userinfo endpoints, ensuring both the validity of the token and the verification of an active session.

Ultimately, striking the right balance between security and user experience is crucial for customer retention. A tedious authentication process might deter users, while a smooth and, when apt, invisible authentication journey can promote longer and more secure engagements within the application.

Liked it? Share it!