feat(backend): Add JWT Authentication

This commit is contained in:
2025-03-24 19:48:43 +01:00
parent baeadaa1ce
commit 8c0908abdd
7 changed files with 125 additions and 14 deletions

View File

@@ -3,3 +3,6 @@ DB_PASS=postgres
DB_HOST=localhost DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_NAME=SetGame DB_NAME=SetGame
JWT_SECRET=your_super_secret_key_which_is_long_enough
JWT_ISSUER=wessel.gg
JWT_AUDIENCE=wessel.gg

View File

@@ -0,0 +1,50 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
public class UserLogin
{
public string Username { get; set; }
public string Password { get; set; }
}
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
[HttpPost("login")]
public IActionResult Login([FromBody] UserLogin user)
{
if (user.Username == "admin" && user.Password == "password")
{
var token = GenerateJwtToken(user.Username);
return Ok(new { token });
}
return Unauthorized();
}
private string GenerateJwtToken(string username)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your_super_secret_key_which_is_long_enough"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "wessel.gg",
audience: "wessel.gg",
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}

View File

@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using backend.Models; using backend.Models;
using Microsoft.AspNetCore.Authorization;
namespace backend.Controllers namespace backend.Controllers
{ {
@@ -20,12 +21,14 @@ namespace backend.Controllers
// GET: api/Games // GET: api/Games
[HttpGet] [HttpGet]
[Authorize]
public async Task<ActionResult<IEnumerable<Game>>> GetGames() { public async Task<ActionResult<IEnumerable<Game>>> GetGames() {
return await _context.Games.ToListAsync(); return await _context.Games.ToListAsync();
} }
// GET: api/Games/5 // GET: api/Games/5
[HttpGet("{id}")] [HttpGet("{id}")]
[Authorize]
public async Task<ActionResult<Game>> GetGame(long id) public async Task<ActionResult<Game>> GetGame(long id)
{ {
var game = await _context.Games.FindAsync(id); var game = await _context.Games.FindAsync(id);
@@ -41,6 +44,7 @@ namespace backend.Controllers
// PUT: api/Games/5 // PUT: api/Games/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")] [HttpPut("{id}")]
[Authorize]
public async Task<IActionResult> PutGame(long id, Game game) public async Task<IActionResult> PutGame(long id, Game game)
{ {
if (id != game.Id) if (id != game.Id)
@@ -66,6 +70,7 @@ namespace backend.Controllers
// POST: api/Games // POST: api/Games
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost] [HttpPost]
[Authorize]
public async Task<ActionResult<Game>> PostGame() { public async Task<ActionResult<Game>> PostGame() {
var newGame = new Game { var newGame = new Game {
Deck = ( Deck = (
@@ -92,6 +97,7 @@ namespace backend.Controllers
// DELETE: api/Games/5 // DELETE: api/Games/5
[HttpDelete("{id}")] [HttpDelete("{id}")]
[Authorize]
public async Task<IActionResult> DeleteGame(long id) { public async Task<IActionResult> DeleteGame(long id) {
var game = await _context.Games.FindAsync(id); var game = await _context.Games.FindAsync(id);
if (game == null) if (game == null)
@@ -111,6 +117,7 @@ namespace backend.Controllers
[HttpPost] [HttpPost]
[Route("[action]/{id}")] [Route("[action]/{id}")]
[Authorize]
public async Task<ActionResult<SetCheckResult>> CheckSet( public async Task<ActionResult<SetCheckResult>> CheckSet(
long id, long id,
[FromBody] ushort[] cardIndices [FromBody] ushort[] cardIndices
@@ -133,6 +140,7 @@ namespace backend.Controllers
[HttpGet] [HttpGet]
[Route("[action]/{id}")] [Route("[action]/{id}")]
[Authorize]
public async Task<ActionResult<List<int[]>>> SetsInHand(long id) { public async Task<ActionResult<List<int[]>>> SetsInHand(long id) {
var game = await _context.Games.FindAsync(id); var game = await _context.Games.FindAsync(id);
if (game == null) { if (game == null) {

View File

@@ -1,9 +1,14 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using backend.Models; using backend.Models;
using DotNetEnv; using DotNetEnv;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
// Load .env file
Env.Load(); Env.Load();
// Set up config and builder, load env into it
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration var config = builder.Configuration
.AddEnvironmentVariables() .AddEnvironmentVariables()
@@ -11,35 +16,52 @@ var config = builder.Configuration
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = string.IsNullOrEmpty(config["JWT_ISSUER"]) ? false : true,
ValidateAudience = string.IsNullOrEmpty(config["JWT_AUDIENCE"]) ? false : true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = config["JWT_ISSUER"],
ValidAudience = config["JWT_AUDIENCE"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["JWT_SECRET"] ?? ""))
};
});
builder.Services.AddAuthorization();
var postgres_connection_string = $@"User ID={config["DB_USER"]}; // Set up database connection
var postgres_connection_string =
$@"User ID={config["DB_USER"]};
Password={config["DB_PASS"]}; Password={config["DB_PASS"]};
Host={config["DB_HOST"]}; Host={config["DB_HOST"]};
Port={config["DB_PORT"]}; Port={config["DB_PORT"]};
Database={config["DB_NAME"]}; Database={config["DB_NAME"]};
Connection Lifetime=0;"; Connection Lifetime=0;
";
builder.Services.AddDbContext<GameContext>(opt => opt.UseNpgsql(postgres_connection_string)); builder.Services.AddDbContext<GameContext>(opt => opt.UseNpgsql(postgres_connection_string));
var app = builder.Build(); var app = builder.Build();
// Disable CORS // Disable CORS
app.Logger.LogInformation("Disabling CORS to allow communication with frontend");
app.UseCors(builder => { app.UseCors(builder => {
builder.AllowAnyOrigin() builder.AllowAnyOrigin()
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader(); .AllowAnyHeader();
}); });
// Enable Swagger UI
if (app.Environment.IsDevelopment()) { if (app.Environment.IsDevelopment()) {
app.Logger.LogInformation("Enabling Swagger UI for development");
app.MapOpenApi(); app.MapOpenApi();
app.UseSwaggerUi(options => options.DocumentPath = "/openapi/v1.json"); app.UseSwaggerUi(options => options.DocumentPath = "/openapi/v1.json");
} }
app.Logger.LogInformation("Creating middleware"); // Enable Middleware
// app.UseHttpsRedirection(); app.UseHttpsRedirection();
// app.UseAuthorization(); app.UseAuthentication();
app.UseAuthorization();
app.MapControllers(); app.MapControllers();
// Start the server
app.Run(); app.Run();

View File

@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DotNetEnv" Version="3.1.1" /> <PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -3,6 +3,8 @@ import { Card, toCard } from '../../app/models/card';
// todo: Rewrite to use angular http client instead of axios, supports always sending tokens // todo: Rewrite to use angular http client instead of axios, supports always sending tokens
import axios from 'axios'; import axios from 'axios';
axios.defaults.headers.common['Authorization'] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImp0aSI6ImRmNThhYTU3LWZkNzItNGIzYS05OTNmLTY4NjAyNGMzYjdlNSIsImV4cCI6MTc0MjgxODEzOCwiaXNzIjoid2Vzc2VsLmdnIiwiYXVkIjoid2Vzc2VsLmdnIn0.hDf8qcxXeSFQhmgnMzBrH3ZJJMplwZ-1RQwNxeZo5ok';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class GameService { export class GameService {

View File

@@ -1,15 +1,40 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import axios, { AxiosError } from 'axios'; import axios, { AxiosError } from 'axios';
// axios.defaults.headers.common['Authorization'] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImp0aSI6ImRmNThhYTU3LWZkNzItNGIzYS05OTNmLTY4NjAyNGMzYjdlNSIsImV4cCI6MTc0MjgxODEzOCwiaXNzIjoid2Vzc2VsLmdnIiwiYXVkIjoid2Vzc2VsLmdnIn0.hDf8qcxXeSFQhmgnMzBrH3ZJJMplwZ-1RQwNxeZo5ok';
axios.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class UserDataService { export class UserDataService {
constructor() { } constructor() {
this.login({ username: 'admin', password: 'password' });
}
public async login(credentials: any) {
const response = await axios.post('http://localhost:5224/api/Auth/login', credentials);
localStorage.setItem('token', response.data.token);
}
public async getGames(): Promise<any> { public async getGames(): Promise<any> {
const req = await axios.get('http://localhost:5224/api/v1/Games'); const req = await axios.get('http://localhost:5224/api/v1/Games', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImp0aSI6ImRmNThhYTU3LWZkNzItNGIzYS05OTNmLTY4NjAyNGMzYjdlNSIsImV4cCI6MTc0MjgxODEzOCwiaXNzIjoid2Vzc2VsLmdnIiwiYXVkIjoid2Vzc2VsLmdnIn0.hDf8qcxXeSFQhmgnMzBrH3ZJJMplwZ-1RQwNxeZo5ok'
}
});
const sortedGames = req.data const sortedGames = req.data
.sort((a: any, b: any) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime()) .sort((a: any, b: any) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime())
.reverse(); .reverse();