Lewati ke konten utama

Token Lifecycle

Token lifecycle — provisioning, rotation, expiry, dan revocation kredensial API.

Overview​

cMind mengelola beberapa tipe token:

TipePenggunaanLifetime
JWTAPI authentication5 menit
Refresh TokenMendapatkan JWT baru30 hari
OAuth TokencTrader Open API1 jam
MCP TokenMCP server accessKonfigurabel

JWT Tokens​

Generation​

JWT dihasilkan saat login:

public string GenerateJwt(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Email, user.Email),
new Claim("role", user.Role)
};

var key = new SymmetricSecurityKey(_jwtSecret);
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var token = new JwtSecurityToken(
issuer: "cmind",
audience: "cmind-api",
claims: claims,
expires: DateTime.UtcNow.AddMinutes(5),
signingCredentials: creds
);

return new JwtSecurityTokenHandler().WriteToken(token);
}

Validation​

public async Task<ClaimsPrincipal> ValidateJwt(string token)
{
var handler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(_jwtSecret);

var parameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "cmind",
ValidateAudience = true,
ValidAudience = "cmind-api",
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};

return await handler.ValidateAsync(token, parameters);
}

Refresh Tokens​

Rotation​

Refresh token di-rotasi setiap use:

public async Task<TokenPair> RotateRefreshToken(string refreshToken)
{
var userId = await ValidateRefreshToken(refreshToken);

// Invalidate old token
await _db.RefreshTokens
.Where(t => t.Token == refreshToken)
.DeleteAsync();

// Generate new pair
var jwt = GenerateJwt(userId);
var newRefresh = GenerateRefreshToken();
await SaveRefreshToken(userId, newRefresh);

return new TokenPair(jwt, newRefresh);
}

Storage​

Token di-hash sebelum disimpan:

public async Task SaveRefreshToken(string userId, string token)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(token));
var entity = new RefreshToken
{
UserId = userId,
TokenHash = Convert.ToBase64String(hash),
ExpiresAt = DateTime.UtcNow.AddDays(30),
CreatedAt = DateTime.UtcNow
};
await _db.RefreshTokens.AddAsync(entity);
}

OAuth Tokens​

cTrader Open API​

public class OAuthTokenManager
{
public async Task<OAuthToken> RefreshOAuthTokenAsync(string refreshToken)
{
var response = await _httpClient.PostAsync("https://api.spotware.com/oauth/token",
new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("refresh_token", refreshToken),
new KeyValuePair<string, string>("client_id", _clientId),
new KeyValuePair<string, string>("client_secret", _clientSecret)
}));

var token = await response.Content.ReadFromJsonAsync<OAuthToken>();
await SaveTokenAsync(token);
return token;
}
}

Automatic Refresh​

Token di-refresh sebelum expiry:

public class TokenRefreshBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var expiringTokens = await _db.OAuthTokens
.Where(t => t.ExpiresAt < DateTime.UtcNow.AddMinutes(5))
.ToListAsync();

foreach (var token in expiringTokens)
{
await _tokenManager.RefreshOAuthTokenAsync(token.RefreshToken);
}

await Task.Delay(TimeSpan.FromMinutes(1), ct);
}
}
}

MCP Tokens​

Generation​

public string GenerateMcpToken(User user)
{
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
}

Scoping​

public class McpToken
{
public string Token { get; set; }
public string UserId { get; set; }
public string[] Scopes { get; set; } // "trading:read", "trading:write", "backtest:run"
public DateTime ExpiresAt { get; set; }
}

Revocation​

Manual Revocation​

public async Task RevokeToken(string token)
{
await _db.RevokedTokens.AddAsync(new RevokedToken
{
TokenHash = Hash(token),
RevokedAt = DateTime.UtcNow,
Reason = "user_requested"
});
}

Automatic Revocation​

// Revoke all tokens for user
public async Task RevokeAllUserTokens(string userId)
{
await _db.RefreshTokens.Where(t => t.UserId == userId).DeleteAsync();
await _db.McpTokens.Where(t => t.UserId == userId).DeleteAsync();
await _db.RevokedTokens.AddRangeAsync(GetActiveTokensForUser(userId).Select(t => new RevokedToken
{
TokenHash = Hash(t),
RevokedAt = DateTime.UtcNow,
Reason = "user_logout_all"
}));
}

Token Blacklist Check​

public async Task<bool> IsTokenRevoked(string token)
{
var hash = Hash(token);
return await _db.RevokedTokens.AnyAsync(t => t.TokenHash == hash);
}

Monitoring​

Dashboard menampilkan:

  • Active tokens — jumlah token aktif per user.
  • Expiring soon — token akan expire dalam 24 jam.
  • Revoked tokens — token yang di-revoke.
  • Token usage — pattern penggunaan token.