Skip to content

Instantly share code, notes, and snippets.

@pedroinfo
Created May 23, 2025 12:26
Show Gist options
  • Save pedroinfo/fdc3489c09a0b3a30a410520ea3a7633 to your computer and use it in GitHub Desktop.
Save pedroinfo/fdc3489c09a0b3a30a410520ea3a7633 to your computer and use it in GitHub Desktop.
// -----------------------------
// 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