When building secure APIs, implementing authentication and session management is crucial. Access tokens and refresh tokens are common solutions to ensure security, scalability, and user-friendly experiences. In this blog, we’ll explain how to use these tokens in a .NET WebForms application with step-by-step guidance.
Understanding Access and Refresh Tokens
Access Token
- A short-lived token (e.g., 5 minute) used to authenticate API requests.
- Passed in the
Authorizationheader of every API call.
Refresh Token
- A long-lived token (e.g., 3 days) used to generate a new access token.
- Stored securely (e.g., database) and only sent to specific endpoints.
By combining these tokens, you can secure your APIs and provide a seamless experience for users.
Database Setup
Create a table to store refresh tokens:
CREATE TABLE RefreshTokens (
id INT PRIMARY KEY IDENTITY,
userId INT NOT NULL,
token NVARCHAR(500) NOT NULL,
expiresAt DATETIME NOT NULL,
createdAt DATETIME DEFAULT getdate(),
revokedAt DATETIME NULL,
isRevoked BIT DEFAULT 0
); Stored procedure to insert a token
Create procedure [usp_insertRefreshToken](@userId int, @refreshToken nvarchar(500))
as
begin
SET NOCOUNT ON;
INSERT INTO RefreshTokens (userId, token, expiresAt, createdAt, revokedAt, isRevoked)
VALUES (
@userId,
@refreshToken,
DATEADD(DAY, 3, getdate()),
getdate,
NULL,
0
);
end
Stored Procedure to Invalidate Refresh Tokens
CREATE PROCEDURE usp_InvalidateRefreshToken
@token NVARCHAR(500)
AS
BEGIN
UPDATE RefreshTokens
SET isRevoked = 1, revokedAt = getdate()
WHERE token = @token AND isRevoked = 0;
END;
First add JWT Support
Install the required NuGet package:
Use the System.IdentityModel.Tokens.Jwt library to handle JWT creation and validation.
Run this command in the Package Manager Console:
Install-Package System.IdentityModel.Tokens.Jwt
Implementing Token Generation
Use the JwtSecurityToken class to generate an access token:
public static string GenerateAccessToken(string userId)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: issuer,
audience: audience,
claims: new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim("role", "user")
},
expires: DateTime.UtcNow.AddMinutes(15), //customize this expiry
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Refresh Token Generation
Generate a random refresh token:
public static string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
}
return Convert.ToBase64String(randomNumber);
}
Save the refresh token to the database:
public void SaveRefreshToken(int userId, string refreshToken)
{
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["yourconnectionstring"].ConnectionString))
{
using (SqlCommand cmd = new SqlCommand("INSERT INTO RefreshTokens
(userId, token, expiresAt) VALUES (@userId, @token, @expiresAt)", conn))
{
cmd.Parameters.AddWithValue("@userId", userId);
cmd.Parameters.AddWithValue("@token", refreshToken);
cmd.Parameters.AddWithValue("@expiresAt", DateTime.UtcNow.AddDays(3));
conn.Open();
cmd.ExecuteNonQuery();
}
}
}
Token Validation
Validating Access Tokens
To verify access tokens on every API request:
public static ClaimsPrincipal ValidateAccessToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(secretKey);
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidIssuer = issuer,
ValidAudience = audience,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
try
{
SecurityToken validatedToken;
return tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
}
catch
{
return null; // Token is invalid
}
}
To revoke a refresh token:
public bool InvalidateRefreshToken(string refreshToken)
{
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["yourconnectionstring"].ConnectionString))
{
using (SqlCommand cmd = new SqlCommand("usp_InvalidateRefreshToken", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("@token", refreshToken);
conn.Open();
int rowsAffected = cmd.ExecuteNonQuery();
return rowsAffected > 0;
}
}
}
Integrating Token Verification in APIs
string authHeader = Request.Headers["Authorization"];
if (authHeader == null || JWTHandler.ValidateAccessToken(authHeader.Replace("Bearer ", "")) == null)
{
throw new UnauthorizedAccessException("Invalid or missing token.");
}
Refreshing Access Tokens
If the access token expires, generate a new one using the refresh token:
public string RefreshAccessToken(string refreshToken)
{
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["dbConnection"].ConnectionString))
{
string query = "SELECT userId, expiresAt, isRevoked FROM RefreshTokens WHERE token = @token";
using (SqlCommand cmd = new SqlCommand(query, conn))
{
cmd.Parameters.AddWithValue("@token", refreshToken);
conn.Open();
using (var reader = cmd.ExecuteReader())
{
if (reader.Read() && !(bool)reader["isRevoked"] && (DateTime)reader["expiresAt"] > DateTime.UtcNow)
{
string userId = reader["userId"].ToString();
return JWTHandler.GenerateAccessToken(userId);
}
else
{
throw new UnauthorizedAccessException("Invalid or expired refresh token.");
}
}
}
}
}
Conclusion
By combining access and refresh tokens, you can ensure secure, scalable APIs while enhancing the user experience. This approach minimizes the risk of unauthorized access and provides a reliable mechanism for session management. With the above implementation, your .NET WebForms application is ready to handle secure API requests seamlessly.
