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

> Discover how CoSec implements JWTs for secure access and refresh token management. Learn about stateless architecture, token generation, and validation.

- Repository: [Ahoo Wang/cosec](https://github.com/ahoo-wang/cosec)
- Tags: how-to-guide
- Published: 2026-02-23

---

**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`](https://github.com/ahoo-wang/cosec/blob/main/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.

```kotlin
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`](https://github.com/ahoo-wang/cosec/blob/main/cosec-core/src/main/kotlin/me/ahoo/cosec/token/TokenVerifier.kt), this interface handles validation and refresh operations.

```kotlin
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`](https://github.com/ahoo-wang/cosec/blob/main/cosec-spring-boot-starter/src/main/kotlin/me/ahoo/cosec/spring/boot/starter/jwt/JwtProperties.kt).

```kotlin
@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`](https://github.com/ahoo-wang/cosec/blob/main/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`.

```kotlin
// 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`](https://github.com/ahoo-wang/cosec/blob/main/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`.

```kotlin
// 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.

```kotlin
// 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`](https://github.com/ahoo-wang/cosec/blob/main/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.