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:
- Verifies the refresh JWT signature and expiration
- Decodes the (potentially expired) access JWT without verification using
Jwts.decode - Validates that
refreshToken.subject == accessToken.id - Returns the original
TokenPrincipalonly 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
JwtTokenConverterandJwtTokenVerifierclasses in thecosec-jwtmodule. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →