add refresh tokens
All checks were successful
Build and Deploy API / build-and-deploy (push) Successful in 9s

This commit is contained in:
2026-03-22 16:48:58 -05:00
parent 31db3bc58c
commit 74307e614c
7 changed files with 497 additions and 3 deletions

View File

@@ -51,9 +51,18 @@ public class AuthController : ControllerBase {
if(!result.Succeeded) return Unauthorized();
var token = jwt_.GenerateJwt(user);
var accessToken = jwt_.GenerateJwt(user);
var refreshToken = jwt_.GenerateRefreshToken();
RefreshToken newTokenObject = new RefreshToken {
Token = refreshToken,
UserId = user.Id,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddDays(30),
IsRevoked = false
};
await jwt_.AddRefreshToken(newTokenObject);
return Ok(new { token });
return Ok(new { accessToken, refreshToken });
}
[Authorize] // authorize is handled by middleware
@@ -65,6 +74,37 @@ public class AuthController : ControllerBase {
return Ok();
}
[HttpPost("refresh")] // allow-anonymous by default
public async Task<ActionResult> Refresh(TokenDto request) {
RefreshToken? storedToken = await jwt_.GetRefreshToken(request.RefreshToken);
if (storedToken == null) return Unauthorized();
bool valid = (storedToken.IsRevoked) ||
(storedToken.ExpiresAt < DateTime.UtcNow);
if(!valid) return Unauthorized(); // TODO: delete the invalid token
User? user = await jwt_.GetUser(storedToken.UserId);
if(user == null) return NotFound();
string? newAccessToken = jwt_.GenerateJwt(user);
if(newAccessToken == null) return NotFound();
string newRefreshToken = jwt_.GenerateRefreshToken();
storedToken.IsRevoked = true;
RefreshToken newTokenObject = new RefreshToken {
Token = newRefreshToken,
UserId = storedToken.UserId,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddDays(30),
IsRevoked = false
};
await jwt_.AddRefreshToken(newTokenObject);
return Ok(new { accessToken = newAccessToken, refreshToken = newRefreshToken });
}
// TODO
// refresh tokens
// email verification

View File

@@ -12,5 +12,6 @@ public class AppDbContext : IdentityDbContext<User> {
// Db set for each model besides Users (DbSet<template> is already defined in IdenityDbContext<template>)
public DbSet<Item> Items { get; set; }
public DbSet<RefreshToken> RefreshTokens { get; set; }
}

View File

@@ -0,0 +1,21 @@
// a refresh token's purpose is to authenticate user's without logging in
public class RefreshToken {
public int Id { get; set; }
public string Token { get; set; } = "";
public string UserId { get; set; } = ""; // in EF Identity the IdentityUser's id is a GUID string (32 hex digits)
public DateTime CreatedAt { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsRevoked { get; set; }
}
public class TokenDto {
public string RefreshToken { get; set; } = "";
}

View File

@@ -1,17 +1,21 @@
using Microsoft.IdentityModel.Tokens;
using Microsoft.EntityFrameworkCore;
using System.Text;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using agologumApi.Models;
public class JwtService {
private readonly IConfiguration config_;
private readonly AppDbContext db_;
public JwtService(IConfiguration config) { // why the heck does c# not have initializer lists ?
public JwtService(IConfiguration config, AppDbContext db) { // why the heck does c# not have initializer lists ?
config_ = config;
db_ = db;
}
public string? GenerateJwt(User user) {
@@ -42,5 +46,27 @@ public class JwtService {
}
public string GenerateRefreshToken() {
byte[] randomBytes = new byte[64];
RandomNumberGenerator.Fill(randomBytes.AsSpan());
return Convert.ToBase64String(randomBytes);
}
public async Task<RefreshToken?> GetRefreshToken(string refreshTokenString) {
return await db_.RefreshTokens.FirstOrDefaultAsync(u => u.Token == refreshTokenString);
}
public async Task<RefreshToken> AddRefreshToken(RefreshToken refreshToken) {
db_.RefreshTokens.Add(refreshToken);
await db_.SaveChangesAsync();
return refreshToken;
}
// helper to get the User from the id that exists in a refresh token object
public async Task<User?> GetUser(string id) {
return await db_.Users.FindAsync(id);
}
}