How CoSec Manages Access Tokens and Refresh Tokens: JWT Implementation Guide

CoSec manages access tokens and refresh tokens using a stateless JWT architecture where JwtTokenConverter generates cryptographically signed token pairs with independent expiration times, while JwtTokenVerifier validates requests and handles token refresh by binding each refresh token to a specific access token ID.

The ahoo-wang/cosec repository implements a comprehensive security framework that eliminates server-side session storage through self-contained JSON Web Tokens. This approach to CoSec access tokens and refresh tokens ensures scalable, stateless authentication suitable for distributed systems and microservices architectures.

Core Architecture and Components

CoSec’s token management strategy separates concerns across three primary components defined in the cosec-core and cosec-jwt modules. All implementations rely on the JWT standard with configurable signing algorithms, defaulting to HMAC256.

TokenConverter Interface

The TokenConverter interface in cosec-core/src/main/kotlin/me/ahoo/cosec/token/TokenConverter.kt defines the contract for transforming a CoSecPrincipal into a CompositeToken containing both access and refresh tokens.

interface TokenConverter {
    fun toToken(principal: CoSecPrincipal): CompositeToken
    fun toToken(principal: CoSecPrincipal,
                accessTokenValidity: Duration,
                refreshTokenValidity: Duration): CompositeToken
}

TokenVerifier Interface

Located in cosec-core/src/main/kotlin/me/ahoo/cosec/token/TokenVerifier.kt, this interface handles validation and refresh operations.

interface TokenVerifier : PrincipalConverter {
    fun <T : TokenPrincipal> verify(accessToken: AccessToken): T
    fun <T : TokenPrincipal> refresh(token: CompositeToken): T
}

JwtProperties Configuration

Token lifetimes and cryptographic settings are centralized in cosec-spring-boot-starter/src/main/kotlin/me/ahoo/cosec/spring/boot/starter/jwt/JwtProperties.kt.

@ConfigurationProperties(prefix = "cosec.jwt")
class JwtProperties(
    var algorithm: Algorithm = Algorithm.HMAC256,
    var secret: String,
    @NestedConfigurationProperty var tokenValidity: TokenValidity = TokenValidity()
)

Default validity periods set access tokens to expire after 10 minutes and refresh tokens after 7 days.

Token Generation with JwtTokenConverter

The concrete implementation in cosec-jwt/src/main/kotlin/me/ahoo/cosec/jwt/JwtTokenConverter.kt creates a CompositeToken through a multi-step process that ensures cryptographic separation between access and refresh tokens.

Access Token Structure

When converting a principal, the converter generates a unique access-token ID using idGenerator.generateAsString(). The resulting JWT contains:

  • Subject: The principal's ID
  • Claims: Policies, roles, attributes, and tenant ID
  • Expiration: now + accessTokenValidity
  • JWT ID: The generated access-token ID

Refresh Token Binding

The refresh token is constructed with a critical security constraint: its subject field contains only the access-token ID, not the principal data. This creates a cryptographic binding where the refresh token references exactly one access token. The refresh token receives an independent expiration of now + refreshTokenValidity.

// Generate tokens after successful authentication
val principal: CoSecPrincipal = // … build from user data
val tokenConverter: TokenConverter = JwtTokenConverter(
    idGenerator = IdGenerator.default(),
    algorithm = Algorithm.HMAC256(jwtProperties.secret)
)
val compositeToken: CompositeToken = tokenConverter.toToken(principal)
// compositeToken.accessToken  -> JWT string for API calls
// compositeToken.refreshToken -> JWT string for refreshing

Token Verification and Refresh Flow

Access Token Validation

The JwtTokenVerifier class in cosec-jwt/src/main/kotlin/me/ahoo/cosec/jwt/JwtTokenVerifier.kt handles incoming requests. The verify method strips the Bearer prefix, validates the JWT signature and expiration, then maps the decoded token to a TokenPrincipal using Jwts.toPrincipal.

If the token is expired, the verifier throws TokenExpiredException. Any signature mismatch or structural issue results in TokenVerificationException.

// Verify an incoming access token (e.g., in a filter)
val tokenVerifier: TokenVerifier = JwtTokenVerifier(Algorithm.HMAC256(jwtProperties.secret))
val authHeader = request.getHeader("Authorization") // "Bearer eyJhbGci..."
val accessToken = AccessToken(authHeader)           // simple wrapper
val principalFromToken = tokenVerifier.verify<TokenPrincipal>(accessToken)

Secure Token Refresh Mechanism

The refresh flow implements a binding verification pattern to prevent token theft and replay attacks. When jwtTokenVerifier.refresh(compositeToken) is called:

  1. Verifies the refresh JWT signature and expiration
  2. Decodes the (potentially expired) access JWT without verification using Jwts.decode
  3. Validates that refreshToken.subject == accessToken.id
  4. Returns the original TokenPrincipal only if the binding matches

This ensures a refresh token issued for one access token cannot be used to renew a different session.

// Refresh when access token has expired
val refreshToken = AccessToken("Bearer ${compositeToken.refreshToken}")
val refreshedPrincipal = tokenVerifier.refresh<TokenPrincipal>(
    CompositeToken(compositeToken.accessToken, compositeToken.refreshToken)
)
// Issue a new access token if desired
val newToken = tokenConverter.toToken(refreshedPrincipal)

Stateless Security Benefits

By implementing TokenConverter and TokenVerifier as interfaces, CoSec allows alternative implementations (such as opaque tokens) without modifying consuming code. The JWT-based approach stores all session state within the tokens themselves, eliminating database lookups during request processing and enabling horizontal scaling without shared session storage.

Summary

  • CoSec access tokens and refresh tokens are managed through the JwtTokenConverter and JwtTokenVerifier classes in the cosec-jwt module.
  • Access tokens carry full principal claims with short lifespans (default 10 minutes), while refresh tokens contain only the access-token ID with longer expiration (default 7 days).
  • The refresh mechanism cryptographically binds refresh tokens to specific access tokens by matching the refresh token's subject to the access token's JWT ID.
  • All configuration is centralized in JwtProperties, allowing customization of algorithms, secrets, and validity periods without code changes.
  • The architecture is fully stateless, storing no session data on the server between requests.

Frequently Asked Questions

How does CoSec prevent refresh token abuse across different sessions?

CoSec mitigates refresh token abuse through cryptographic binding. In JwtTokenVerifier.refresh, the implementation verifies that the refresh token's subject claim exactly matches the access token's ID claim (refreshToken.subject == accessToken.id). This ensures a refresh token can only renew the specific access token it was issued alongside, preventing stolen refresh tokens from being used to escalate privileges or hijack unrelated sessions.

What happens when a CoSec access token expires?

When a CoSec access token expires, the JwtTokenVerifier.verify method throws a TokenExpiredException. The client must then present both the expired access token and the valid refresh token to the refresh endpoint. The verifier decodes the expired access token without signature validation (since it has expired) but uses the refresh token's valid signature to confirm the binding before returning the principal for new token issuance.

Can I customize the token expiration times in CoSec?

Yes, expiration times are fully configurable through the JwtProperties class in the Spring Boot starter. You can define accessTokenValidity and refreshTokenValidity as Duration values in your application configuration. The defaults are 10 minutes for access tokens and 7 days for refresh tokens, but these can be adjusted per deployment environment or even overridden per token generation call using the overloaded TokenConverter.toToken method that accepts explicit validity durations.

Which signing algorithms does CoSec support for JWT tokens?

According to the source code in JwtProperties.kt, CoSec defaults to Algorithm.HMAC256 but supports any algorithm provided by the underlying JWT library. The algorithm property in JwtProperties accepts any Algorithm implementation, allowing you to configure HMAC384, HMAC512, RSA256, or ECDSA variants depending on your security requirements and key management infrastructure.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →