Created
May 23, 2025 12:26
-
-
Save pedroinfo/fdc3489c09a0b3a30a410520ea3a7633 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ----------------------------- | |
// TokenInfo.cs | |
// ----------------------------- | |
public class TokenInfo | |
{ | |
public string Token { get; init; } | |
public string Username { get; init; } | |
public string IpAddress { get; init; } | |
public DateTime Expiration { get; init; } | |
} | |
// ----------------------------- | |
// TokenValidationStatus.cs | |
// ----------------------------- | |
public enum TokenValidationStatus | |
{ | |
Valid, | |
TokenNotFound, | |
TokenExpired, | |
UsernameMismatch, | |
IpMismatch | |
} | |
// ----------------------------- | |
// TokenValidationResult.cs | |
// ----------------------------- | |
public class TokenValidationResult | |
{ | |
public bool Success => Status == TokenValidationStatus.Valid; | |
public TokenValidationStatus Status { get; set; } | |
public string Message { get; set; } | |
} | |
// ----------------------------- | |
// ITokenStore.cs | |
// ----------------------------- | |
using System.Threading.Tasks; | |
public interface ITokenStore | |
{ | |
Task StoreAsync(TokenInfo tokenInfo); | |
Task<TokenInfo> GetAsync(string token); | |
Task RemoveAsync(string token); | |
} | |
// ----------------------------- | |
// InMemoryTokenStore.cs | |
// ----------------------------- | |
using System.Collections.Concurrent; | |
using System.Threading.Tasks; | |
public class InMemoryTokenStore : ITokenStore | |
{ | |
private readonly ConcurrentDictionary<string, TokenInfo> _tokens = new(); | |
public Task StoreAsync(TokenInfo tokenInfo) | |
{ | |
_tokens[tokenInfo.Token] = tokenInfo; | |
return Task.CompletedTask; | |
} | |
public Task<TokenInfo> GetAsync(string token) | |
{ | |
_tokens.TryGetValue(token, out var info); | |
return Task.FromResult(info); | |
} | |
public Task RemoveAsync(string token) | |
{ | |
_tokens.TryRemove(token, out _); | |
return Task.CompletedTask; | |
} | |
} | |
// ----------------------------- | |
// RedisTokenStore.cs | |
// ----------------------------- | |
using StackExchange.Redis; | |
using System.Text.Json; | |
public class RedisTokenStore : ITokenStore | |
{ | |
private readonly IDatabase _db; | |
private readonly string _prefix; | |
public RedisTokenStore(string redisConnectionString, string keyPrefix = "auth_token:") | |
{ | |
var redis = ConnectionMultiplexer.Connect(redisConnectionString); | |
_db = redis.GetDatabase(); | |
_prefix = keyPrefix; | |
} | |
public async Task StoreAsync(TokenInfo tokenInfo) | |
{ | |
string key = _prefix + tokenInfo.Token; | |
string json = JsonSerializer.Serialize(tokenInfo); | |
TimeSpan ttl = tokenInfo.Expiration - DateTime.UtcNow; | |
await _db.StringSetAsync(key, json, ttl); | |
} | |
public async Task<TokenInfo> GetAsync(string token) | |
{ | |
string key = _prefix + token; | |
var value = await _db.StringGetAsync(key); | |
return value.HasValue | |
? JsonSerializer.Deserialize<TokenInfo>(value) | |
: null; | |
} | |
public async Task RemoveAsync(string token) | |
{ | |
string key = _prefix + token; | |
await _db.KeyDeleteAsync(key); | |
} | |
} | |
// ----------------------------- | |
// SqlServerTokenStore.cs | |
// ----------------------------- | |
using Dapper; | |
using System.Data.SqlClient; | |
public class SqlServerTokenStore : ITokenStore | |
{ | |
private readonly string _connectionString; | |
public SqlServerTokenStore(string connectionString) | |
{ | |
_connectionString = connectionString; | |
} | |
public async Task StoreAsync(TokenInfo tokenInfo) | |
{ | |
const string sql = @" | |
IF EXISTS (SELECT 1 FROM TokenStore WHERE Token = @Token) | |
UPDATE TokenStore | |
SET Username = @Username, | |
IpAddress = @IpAddress, | |
Expiration = @Expiration | |
WHERE Token = @Token | |
ELSE | |
INSERT INTO TokenStore (Token, Username, IpAddress, Expiration) | |
VALUES (@Token, @Username, @IpAddress, @Expiration);"; | |
using var connection = new SqlConnection(_connectionString); | |
await connection.ExecuteAsync(sql, tokenInfo); | |
} | |
public async Task<TokenInfo> GetAsync(string token) | |
{ | |
const string sql = "SELECT * FROM TokenStore WHERE Token = @Token"; | |
using var connection = new SqlConnection(_connectionString); | |
return await connection.QuerySingleOrDefaultAsync<TokenInfo>(sql, new { Token = token }); | |
} | |
public async Task RemoveAsync(string token) | |
{ | |
const string sql = "DELETE FROM TokenStore WHERE Token = @Token"; | |
using var connection = new SqlConnection(_connectionString); | |
await connection.ExecuteAsync(sql, new { Token = token }); | |
} | |
} | |
// ----------------------------- | |
// TokenService.cs | |
// ----------------------------- | |
using System; | |
using System.Security.Cryptography; | |
using System.Text; | |
using System.Threading.Tasks; | |
public class TokenService | |
{ | |
private readonly ITokenStore _store; | |
public TokenService(ITokenStore store) | |
{ | |
_store = store; | |
} | |
public async Task<string> GenerateTokenAsync(string username, string ipAddress, int validMinutes = 30) | |
{ | |
string rawToken = $"{username}:{ipAddress}:{Guid.NewGuid()}:{DateTime.UtcNow.Ticks}"; | |
string token = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(rawToken))); | |
var tokenInfo = new TokenInfo | |
{ | |
Token = token, | |
Username = username, | |
IpAddress = ipAddress, | |
Expiration = DateTime.UtcNow.AddMinutes(validMinutes) | |
}; | |
await _store.StoreAsync(tokenInfo); | |
return token; | |
} | |
public async Task<TokenValidationResult> ValidateTokenAsync(string token, string username, string ipAddress) | |
{ | |
var tokenInfo = await _store.GetAsync(token); | |
if (tokenInfo == null) | |
{ | |
return new TokenValidationResult | |
{ | |
Status = TokenValidationStatus.TokenNotFound, | |
Message = "Invalid or unknown token." | |
}; | |
} | |
if (!tokenInfo.Username.Equals(username, StringComparison.OrdinalIgnoreCase)) | |
{ | |
return new TokenValidationResult | |
{ | |
Status = TokenValidationStatus.UsernameMismatch, | |
Message = "Username does not match token." | |
}; | |
} | |
if (tokenInfo.IpAddress != ipAddress) | |
{ | |
return new TokenValidationResult | |
{ | |
Status = TokenValidationStatus.IpMismatch, | |
Message = "IP address not allowed for this token." | |
}; | |
} | |
if (DateTime.UtcNow > tokenInfo.Expiration) | |
{ | |
await _store.RemoveAsync(token); | |
return new TokenValidationResult | |
{ | |
Status = TokenValidationStatus.TokenExpired, | |
Message = "Token has expired." | |
}; | |
} | |
return new TokenValidationResult | |
{ | |
Status = TokenValidationStatus.Valid, | |
Message = "Token is valid." | |
}; | |
} | |
public async Task RevokeTokenAsync(string token) | |
{ | |
await _store.RemoveAsync(token); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment