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

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

View File

@@ -1,9 +1,14 @@
using Microsoft.EntityFrameworkCore;
using backend.Models;
using DotNetEnv;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
// Load .env file
Env.Load();
// Set up config and builder, load env into it
var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration
.AddEnvironmentVariables()
@@ -11,35 +16,52 @@ var config = builder.Configuration
builder.Services.AddOpenApi();
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"]};
Password={config["DB_PASS"]};
Host={config["DB_HOST"]};
Port={config["DB_PORT"]};
Database={config["DB_NAME"]};
Connection Lifetime=0;";
// Set up database connection
var postgres_connection_string =
$@"User ID={config["DB_USER"]};
Password={config["DB_PASS"]};
Host={config["DB_HOST"]};
Port={config["DB_PORT"]};
Database={config["DB_NAME"]};
Connection Lifetime=0;
";
builder.Services.AddDbContext<GameContext>(opt => opt.UseNpgsql(postgres_connection_string));
var app = builder.Build();
// Disable CORS
app.Logger.LogInformation("Disabling CORS to allow communication with frontend");
app.UseCors(builder => {
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
// Enable Swagger UI
if (app.Environment.IsDevelopment()) {
app.Logger.LogInformation("Enabling Swagger UI for development");
app.MapOpenApi();
app.UseSwaggerUi(options => options.DocumentPath = "/openapi/v1.json");
}
app.Logger.LogInformation("Creating middleware");
// app.UseHttpsRedirection();
// app.UseAuthorization();
// Enable Middleware
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// Start the server
app.Run();

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<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.CodeAnalysis.Analyzers" Version="3.11.0">
<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
import axios from 'axios';
axios.defaults.headers.common['Authorization'] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImp0aSI6ImRmNThhYTU3LWZkNzItNGIzYS05OTNmLTY4NjAyNGMzYjdlNSIsImV4cCI6MTc0MjgxODEzOCwiaXNzIjoid2Vzc2VsLmdnIiwiYXVkIjoid2Vzc2VsLmdnIn0.hDf8qcxXeSFQhmgnMzBrH3ZJJMplwZ-1RQwNxeZo5ok';
@Injectable({ providedIn: 'root' })
export class GameService {

View File

@@ -1,15 +1,40 @@
import { Injectable } from '@angular/core';
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({
providedIn: 'root'
})
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> {
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
.sort((a: any, b: any) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime())
.reverse();