Merge pull request 'Getting things setup to support token refresh' (#13) from features/token-refresh into main
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #13
This commit is contained in:
jtom38 2023-07-14 22:11:07 -07:00
commit 5c06865b1f
14 changed files with 879 additions and 29 deletions

View File

@ -16,6 +16,9 @@ trigger:
include: include:
- main - main
event:
exclude:
- pull_request
--- ---
kind: pipeline kind: pipeline
type: docker type: docker

View File

@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newsbot.Collector.Api.Domain.Requests; using Newsbot.Collector.Api.Domain.Requests;
using Newsbot.Collector.Api.Domain.Response; using Newsbot.Collector.Api.Domain.Response;
using Newsbot.Collector.Api.Domain.Results;
using Newsbot.Collector.Api.Services; using Newsbot.Collector.Api.Services;
using Newsbot.Collector.Domain.Dto; using Newsbot.Collector.Domain.Dto;
using Newsbot.Collector.Domain.Entities; using Newsbot.Collector.Domain.Entities;
@ -43,19 +44,7 @@ public class AccountController : ControllerBase
} }
var response = _identityService.Register(user.Email, user.Password); var response = _identityService.Register(user.Email, user.Password);
return CheckIfSuccessful(response);
if (!response.IsSuccessful)
{
return new BadRequestObjectResult( new AuthFailedResponse
{
Errors = response.ErrorMessage
});
}
return new OkObjectResult(new AuthSuccessfulResponse
{
Token = response.Token
});
} }
[HttpPost("login")] [HttpPost("login")]
@ -72,18 +61,30 @@ public class AccountController : ControllerBase
} }
var response = _identityService.Login(request.Email, request.Password); var response = _identityService.Login(request.Email, request.Password);
return CheckIfSuccessful(response);
}
if (!response.IsSuccessful) [HttpPost("refresh")]
public ActionResult RefreshToken([FromBody] UserRefreshTokenRequest request)
{
var response = _identityService.RefreshToken(request.Token ?? "", request.RefreshToken ?? "");
return CheckIfSuccessful(response);
}
private ActionResult CheckIfSuccessful(AuthenticationResult result)
{
if (!result.IsSuccessful)
{ {
return new BadRequestObjectResult( new AuthFailedResponse return new BadRequestObjectResult( new AuthFailedResponse
{ {
Errors = response.ErrorMessage Errors = result.ErrorMessage
}); });
} }
return new OkObjectResult(new AuthSuccessfulResponse return new OkObjectResult(new AuthSuccessfulResponse
{ {
Token = response.Token Token = result.Token,
RefreshToken = result.RefreshToken
}); });
} }
} }

View File

@ -0,0 +1,7 @@
namespace Newsbot.Collector.Api.Domain.Requests;
public class UserRefreshTokenRequest
{
public string? Token { get; set; }
public string? RefreshToken { get; set; }
}

View File

@ -5,4 +5,5 @@ public class AuthSuccessfulResponse
// might want to validate the user before we return the token // might want to validate the user before we return the token
public string? Token { get; set; } public string? Token { get; set; }
public string? RefreshToken { get; set; }
} }

View File

@ -5,6 +5,7 @@ namespace Newsbot.Collector.Api.Domain.Results;
public class AuthenticationResult public class AuthenticationResult
{ {
public string? Token { get; set; } public string? Token { get; set; }
public string? RefreshToken { get; set; }
public bool IsSuccessful { get; set; } public bool IsSuccessful { get; set; }
public IEnumerable<string>? ErrorMessage { get; set; } public IEnumerable<string>? ErrorMessage { get; set; }

View File

@ -50,6 +50,7 @@ builder.Services.AddScoped<IIconsRepository, IconsTable>();
builder.Services.AddScoped<ISourcesRepository, SourcesTable>(); builder.Services.AddScoped<ISourcesRepository, SourcesTable>();
builder.Services.AddScoped<IDiscordNotificationRepository, DiscordNotificationTable>(); builder.Services.AddScoped<IDiscordNotificationRepository, DiscordNotificationTable>();
builder.Services.AddScoped<IUserSourceSubscription, UserSourceSubscriptionTable>(); builder.Services.AddScoped<IUserSourceSubscription, UserSourceSubscriptionTable>();
builder.Services.AddScoped<IRefreshTokenRepository, RefreshTokenTable>();
// Configure Identity // Configure Identity
builder.Services.AddScoped<IIdentityService, IdentityService>(); builder.Services.AddScoped<IIdentityService, IdentityService>();
@ -81,6 +82,16 @@ var jwtSettings = new JwtSettings();
config.Bind(nameof(jwtSettings), jwtSettings); config.Bind(nameof(jwtSettings), jwtSettings);
builder.Services.AddSingleton(jwtSettings); builder.Services.AddSingleton(jwtSettings);
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Secret ?? "")),
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = true
};
builder.Services.AddSingleton(tokenValidationParameters);
builder.Services.AddAuthentication(x => builder.Services.AddAuthentication(x =>
{ {
@ -90,17 +101,9 @@ builder.Services.AddAuthentication(x =>
}).AddJwtBearer(x => }).AddJwtBearer(x =>
{ {
x.SaveToken = true; x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters x.TokenValidationParameters = tokenValidationParameters;
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Secret ?? "")),
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = true
};
}); });
builder.Services.AddSwaggerGen(cfg => builder.Services.AddSwaggerGen(cfg =>
{ {

View File

@ -4,6 +4,9 @@ using System.Text;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Newsbot.Collector.Api.Domain.Results; using Newsbot.Collector.Api.Domain.Results;
using Newsbot.Collector.Database;
using Newsbot.Collector.Domain.Entities;
using Newsbot.Collector.Domain.Interfaces;
using Newsbot.Collector.Domain.Models.Config; using Newsbot.Collector.Domain.Models.Config;
namespace Newsbot.Collector.Api.Services; namespace Newsbot.Collector.Api.Services;
@ -12,17 +15,22 @@ public interface IIdentityService
{ {
AuthenticationResult Register(string email, string password); AuthenticationResult Register(string email, string password);
AuthenticationResult Login(string email, string password); AuthenticationResult Login(string email, string password);
AuthenticationResult RefreshToken(string token, string refreshToken);
} }
public class IdentityService : IIdentityService public class IdentityService : IIdentityService
{ {
private readonly UserManager<IdentityUser> _userManager; private readonly UserManager<IdentityUser> _userManager;
private readonly JwtSettings _jwtSettings; private readonly JwtSettings _jwtSettings;
private readonly TokenValidationParameters _tokenValidationParameters;
private readonly IRefreshTokenRepository _refreshTokenRepository;
public IdentityService(UserManager<IdentityUser> userManager, JwtSettings jwtSettings) public IdentityService(UserManager<IdentityUser> userManager, JwtSettings jwtSettings, TokenValidationParameters tokenValidationParameters, IRefreshTokenRepository refreshTokenRepository)
{ {
_userManager = userManager; _userManager = userManager;
_jwtSettings = jwtSettings; _jwtSettings = jwtSettings;
_tokenValidationParameters = tokenValidationParameters;
_refreshTokenRepository = refreshTokenRepository;
} }
public AuthenticationResult Register(string email, string password) public AuthenticationResult Register(string email, string password)
@ -85,6 +93,116 @@ public class IdentityService : IIdentityService
return GenerateJwtToken(user.Result ?? new IdentityUser()); return GenerateJwtToken(user.Result ?? new IdentityUser());
} }
public AuthenticationResult RefreshToken(string token, string refreshToken)
{
var validatedToken = CheckTokenSigner(token);
if (validatedToken is null)
{
return new AuthenticationResult
{
ErrorMessage = new List<string> { "Invalid Token" }
};
}
// Get the expire datetime of the token
var expiryDateUnix = long.Parse(validatedToken.Claims.Single(x => x.Type == JwtRegisteredClaimNames.Exp).Value);
// generate the unix epoc, add expiry time
var expiryDateTimeUtc = new DateTime(1970, 0, 0, 0, 0, 0, DateTimeKind.Utc)
.AddSeconds(expiryDateUnix);
// if it expires in the future
if (expiryDateTimeUtc > DateTime.Now)
{
return new AuthenticationResult
{
ErrorMessage = new List<string> { "The token has not expired yet" }
};
}
var jti = validatedToken.Claims.Single(x => x.Type == JwtRegisteredClaimNames.Jti).Value;
var storedToken = _refreshTokenRepository.Get(token);
if (storedToken is null)
{
return new AuthenticationResult
{
ErrorMessage = new List<string> { "The refresh token does not exist" }
};
}
if (DateTime.UtcNow > storedToken.ExpiryDate)
{
return new AuthenticationResult
{
ErrorMessage = new List<string> { "The refresh token has expired" }
};
}
if (storedToken.Invalidated)
{
return new AuthenticationResult
{
ErrorMessage = new List<string> { "The token is not valid" }
};
}
if (storedToken.Used)
{
return new AuthenticationResult
{
ErrorMessage = new List<string> { "The token has been used" }
};
}
if (storedToken.JwtId != jti)
{
return new AuthenticationResult
{
ErrorMessage = new List<string> { "The token does not match this JWT" }
};
}
_refreshTokenRepository.UpdateTokenIsUsed(token);
var user = _userManager.FindByIdAsync(validatedToken.Claims.Single(x => x.Type == "id").Value);
user.Wait();
if (user.Result is null)
{
return new AuthenticationResult
{
ErrorMessage = new List<string> { "Unable to find user" }
};
}
return GenerateJwtToken(user.Result);
}
private ClaimsPrincipal? CheckTokenSigner(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var principal = tokenHandler.ValidateToken(token, _tokenValidationParameters, out var validatedToken);
if (IsSecurityTokenValidSecurity(validatedToken))
{
return null;
}
return principal;
}
catch
{
return null;
}
}
private bool IsSecurityTokenValidSecurity(SecurityToken token)
{
return (token is JwtSecurityToken jwtSecurityToken) && jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha512, StringComparison.InvariantCultureIgnoreCase);
}
private AuthenticationResult GenerateJwtToken(IdentityUser user) private AuthenticationResult GenerateJwtToken(IdentityUser user)
{ {
var tokenHandler = new JwtSecurityTokenHandler(); var tokenHandler = new JwtSecurityTokenHandler();
@ -105,10 +223,21 @@ public class IdentityService : IIdentityService
var token = tokenHandler.CreateToken(tokenDescriptor); var token = tokenHandler.CreateToken(tokenDescriptor);
var refreshToken = new RefreshTokenEntity
{
JwtId = token.Id,
UserId = user.Id,
CreatedDate = DateTime.UtcNow,
ExpiryDate = DateTime.UtcNow.AddMonths(6)
};
_refreshTokenRepository.Add(refreshToken);
return new AuthenticationResult return new AuthenticationResult
{ {
IsSuccessful = true, IsSuccessful = true,
Token = tokenHandler.WriteToken(token) Token = tokenHandler.WriteToken(token),
RefreshToken = refreshToken.Token
}; };
} }
} }

View File

@ -10,7 +10,6 @@ namespace Newsbot.Collector.Database;
public class DatabaseContext : IdentityDbContext public class DatabaseContext : IdentityDbContext
{ {
public DbSet<ArticlesEntity> Articles { get; set; } = null!; public DbSet<ArticlesEntity> Articles { get; set; } = null!;
public DbSet<DiscordNotificationEntity> DiscordNotification { get; set; } = null!; public DbSet<DiscordNotificationEntity> DiscordNotification { get; set; } = null!;
public DbSet<DiscordQueueEntity> DiscordQueue { get; set; } = null!; public DbSet<DiscordQueueEntity> DiscordQueue { get; set; } = null!;
public DbSet<DiscordWebhookEntity> DiscordWebhooks { get; set; } = null!; public DbSet<DiscordWebhookEntity> DiscordWebhooks { get; set; } = null!;
@ -18,6 +17,8 @@ public class DatabaseContext : IdentityDbContext
public DbSet<SourceEntity> Sources { get; set; } = null!; public DbSet<SourceEntity> Sources { get; set; } = null!;
public DbSet<UserSourceSubscriptionEntity> UserSourceSubscription { get; set; } = null!; public DbSet<UserSourceSubscriptionEntity> UserSourceSubscription { get; set; } = null!;
public DbSet<RefreshTokenEntity> RefreshTokens { get; set; }
private string ConnectionString { get; set; } = ""; private string ConnectionString { get; set; } = "";

View File

@ -0,0 +1,543 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newsbot.Collector.Database;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Newsbot.Collector.Database.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20230711033558_add_jwt_token_refresh_table")]
partial class add_jwt_token_refresh_table
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.ArticlesEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("AuthorImage")
.HasColumnType("text");
b.Property<string>("AuthorName")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("CodeIsCommit")
.HasColumnType("boolean");
b.Property<bool>("CodeIsRelease")
.HasColumnType("boolean");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("PubDate")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("SourceId")
.HasColumnType("uuid");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Thumbnail")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.HasColumnType("text");
b.Property<string>("Video")
.IsRequired()
.HasColumnType("text");
b.Property<int>("VideoHeight")
.HasColumnType("integer");
b.Property<int>("VideoWidth")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("Articles");
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.DiscordNotificationEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("CodeAllowCommits")
.HasColumnType("boolean");
b.Property<bool>("CodeAllowReleases")
.HasColumnType("boolean");
b.Property<Guid>("DiscordWebHookId")
.HasColumnType("uuid");
b.Property<Guid>("SourceId")
.HasColumnType("uuid");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("DiscordNotification");
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.DiscordQueueEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ArticleId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("DiscordQueue");
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.DiscordWebhookEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Channel")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("Server")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("DiscordWebhooks");
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.IconEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("FileName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Site")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("SourceId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("Icons");
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.RefreshTokenEntity", b =>
{
b.Property<string>("Token")
.HasColumnType("text");
b.Property<DateTime>("CreatedDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExpiryDate")
.HasColumnType("timestamp with time zone");
b.Property<bool>("Invalidated")
.HasColumnType("boolean");
b.Property<string>("JwtId")
.HasColumnType("text");
b.Property<bool>("Used")
.HasColumnType("boolean");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Token");
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.SourceEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Deleted")
.HasColumnType("boolean");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Site")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Source")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("text");
b.Property<string>("YoutubeId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Sources");
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.UserSourceSubscriptionEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("DateAdded")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("SourceId")
.HasColumnType("uuid");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserSourceSubscription");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.RefreshTokenEntity", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("User");
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.UserSourceSubscriptionEntity", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Newsbot.Collector.Database.Migrations
{
/// <inheritdoc />
public partial class add_jwt_token_refresh_table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "RefreshTokens",
columns: table => new
{
Token = table.Column<string>(type: "text", nullable: false),
JwtId = table.Column<string>(type: "text", nullable: true),
CreatedDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ExpiryDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Used = table.Column<bool>(type: "boolean", nullable: false),
Invalidated = table.Column<bool>(type: "boolean", nullable: false),
UserId = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RefreshTokens", x => x.Token);
table.ForeignKey(
name: "FK_RefreshTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_RefreshTokens_UserId",
table: "RefreshTokens",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "RefreshTokens");
}
}
}

View File

@ -365,6 +365,36 @@ namespace Newsbot.Collector.Database.Migrations
b.ToTable("Icons"); b.ToTable("Icons");
}); });
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.RefreshTokenEntity", b =>
{
b.Property<string>("Token")
.HasColumnType("text");
b.Property<DateTime>("CreatedDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExpiryDate")
.HasColumnType("timestamp with time zone");
b.Property<bool>("Invalidated")
.HasColumnType("boolean");
b.Property<string>("JwtId")
.HasColumnType("text");
b.Property<bool>("Used")
.HasColumnType("boolean");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Token");
b.HasIndex("UserId");
b.ToTable("RefreshTokens");
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.SourceEntity", b => modelBuilder.Entity("Newsbot.Collector.Domain.Entities.SourceEntity", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -487,6 +517,15 @@ namespace Newsbot.Collector.Database.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.RefreshTokenEntity", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("User");
});
modelBuilder.Entity("Newsbot.Collector.Domain.Entities.UserSourceSubscriptionEntity", b => modelBuilder.Entity("Newsbot.Collector.Domain.Entities.UserSourceSubscriptionEntity", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User") b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")

View File

@ -0,0 +1,42 @@
using Newsbot.Collector.Domain.Entities;
using Newsbot.Collector.Domain.Interfaces;
namespace Newsbot.Collector.Database.Repositories;
public class RefreshTokenTable : IRefreshTokenRepository
{
private readonly DatabaseContext _context;
public RefreshTokenTable(string connectionString)
{
_context = new DatabaseContext(connectionString);
}
public RefreshTokenTable(DatabaseContext context)
{
_context = context;
}
public void Add(RefreshTokenEntity entity)
{
_context.RefreshTokens.Add(entity);
_context.SaveChanges();
}
public RefreshTokenEntity? Get(string token)
{
return _context.RefreshTokens.SingleOrDefault(x => x.Token == token);
}
public void UpdateTokenIsUsed(string token)
{
var entity = Get(token);
if (entity is null)
{
return;
}
entity.Used = true;
_context.RefreshTokens.Update(entity);
_context.SaveChanges();
}
}

View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Identity;
namespace Newsbot.Collector.Domain.Entities;
public class RefreshTokenEntity
{
[Key]
public string? Token { get; set; }
public string? JwtId { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ExpiryDate { get; set; }
public bool Used { get; set; }
public bool Invalidated { get; set; }
public string? UserId { get; set; }
[ForeignKey(nameof(UserId))]
public IdentityUser? User { get; set; }
}

View File

@ -0,0 +1,10 @@
using Newsbot.Collector.Domain.Entities;
namespace Newsbot.Collector.Domain.Interfaces;
public interface IRefreshTokenRepository
{
void Add(RefreshTokenEntity entity);
public RefreshTokenEntity? Get(string token);
void UpdateTokenIsUsed(string token);
}