Working OAuth flow

master
D4VID 1 year ago
parent af7b113035
commit e6e6c73471

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="db" uuid="badf8e21-4e9b-45e6-a672-66a74dc1a74c">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/OAuthServer/db.sqlite3</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="SQLite" />
</component>
</project>

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/SweaWarningsMode/@EntryValue">ShowAndRun</s:String></wpf:ResourceDictionary>

@ -4,20 +4,16 @@ using Microsoft.EntityFrameworkCore;
namespace OAuthServer; namespace OAuthServer;
public class AppDbContext : IdentityDbContext public class AppDbContext : IdentityDbContext {
{ public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) {
{
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
// Setup Identity roles // Setup Identity roles
modelBuilder.Entity<IdentityRole>().HasData( modelBuilder.Entity<IdentityRole>().HasData(
new IdentityRole { Id = Guid.NewGuid().ToString(), Name = "User", NormalizedName = "USER" }, new IdentityRole {Id = Guid.NewGuid().ToString(), Name = "User", NormalizedName = "USER"},
new IdentityRole { Id = Guid.NewGuid().ToString(), Name = "External", NormalizedName = "EXTERNAL" } new IdentityRole {Id = Guid.NewGuid().ToString(), Name = "External", NormalizedName = "EXTERNAL"}
); );
} }
} }

@ -6,27 +6,30 @@ namespace OAuthServer.Controllers;
[ApiController] [ApiController]
[Route("")] [Route("")]
public class ExternalController : ControllerBase public class ExternalController : ControllerBase {
{
private readonly ILogger<ExternalController> _logger; private readonly ILogger<ExternalController> _logger;
public ExternalController(ILogger<ExternalController> logger) public ExternalController(ILogger<ExternalController> logger) {
{
_logger = logger; _logger = logger;
} }
[HttpPost] [HttpPost]
[Authorize(Policy = "External")] [Authorize(Policy = "External")]
[Route("points")] [Route("points")]
public ActionResult PostPoints(int points) public ActionResult PostPoints(int points) {
{
var id = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier); var id = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
if (id == null) if (id == null) {
{
return BadRequest(); return BadRequest();
} }
_logger.LogInformation("User {} got {} points", id.Value, points); _logger.LogInformation("User {} got {} points", id.Value, points);
return Ok(); return Ok();
} }
[HttpGet]
[Authorize(Policy = "External")]
[Route("user")]
public ActionResult GetUser() {
return Ok(new {UserId = 1});
}
} }

@ -7,13 +7,11 @@ namespace OAuthServer.Controllers;
[ApiController] [ApiController]
[Route("")] [Route("")]
public class LoginController : ControllerBase public class LoginController : ControllerBase {
{
private readonly SignInManager<IdentityUser> _signInManager; private readonly SignInManager<IdentityUser> _signInManager;
private readonly UserManager<IdentityUser> _userManager; private readonly UserManager<IdentityUser> _userManager;
public LoginController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager) public LoginController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager) {
{
_signInManager = signInManager; _signInManager = signInManager;
_userManager = userManager; _userManager = userManager;
} }
@ -22,22 +20,18 @@ public class LoginController : ControllerBase
[HttpPost] [HttpPost]
[Route("register")] [Route("register")]
public async Task<ActionResult<string>> Register([FromBody] RegisterRequest registerRequest) public async Task<ActionResult<string>> Register([FromBody] RegisterRequest registerRequest) {
{ IdentityUser user = new IdentityUser {
IdentityUser user = new IdentityUser
{
UserName = registerRequest.Username, UserName = registerRequest.Username,
}; };
IdentityResult registerResult = await _userManager.CreateAsync(user, registerRequest.Password); IdentityResult registerResult = await _userManager.CreateAsync(user, registerRequest.Password);
if (!registerResult.Succeeded) if (!registerResult.Succeeded) {
{
return BadRequest(registerResult); return BadRequest(registerResult);
} }
IdentityResult roleResult = await _userManager.AddToRoleAsync(user, "User"); IdentityResult roleResult = await _userManager.AddToRoleAsync(user, "User");
if (!roleResult.Succeeded) if (!roleResult.Succeeded) {
{
throw new Exception($"Adding role User for {registerRequest.Username} not successful: {roleResult}"); throw new Exception($"Adding role User for {registerRequest.Username} not successful: {roleResult}");
} }
@ -46,8 +40,7 @@ public class LoginController : ControllerBase
[HttpGet] [HttpGet]
[Route("login")] [Route("login")]
public ContentResult Login() public ContentResult Login() {
{
return Content(""" return Content("""
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -72,18 +65,15 @@ public class LoginController : ControllerBase
[HttpPost] [HttpPost]
[Route("login")] [Route("login")]
public async Task<ActionResult> Login([FromForm] LoginRequest loginRequest, string? returnUrl) public async Task<ActionResult> Login([FromForm] LoginRequest loginRequest, string? returnUrl) {
{
SignInResult result = await _signInManager.PasswordSignInAsync(loginRequest.Username, loginRequest.Password, SignInResult result = await _signInManager.PasswordSignInAsync(loginRequest.Username, loginRequest.Password,
isPersistent: true, lockoutOnFailure: false); isPersistent: true, lockoutOnFailure: false);
if (result.Succeeded) if (result.Succeeded) {
{
return Redirect(returnUrl ?? "/"); return Redirect(returnUrl ?? "/");
} }
if (result.IsLockedOut) if (result.IsLockedOut) {
{
return Unauthorized("Account disabled"); return Unauthorized("Account disabled");
} }
@ -92,8 +82,7 @@ public class LoginController : ControllerBase
[HttpPost] [HttpPost]
[Route("logout")] [Route("logout")]
public async Task<ActionResult> Logout() public async Task<ActionResult> Logout() {
{
await _signInManager.SignOutAsync(); await _signInManager.SignOutAsync();
return Ok("Successfully logged out"); return Ok("Successfully logged out");
} }

@ -1,24 +1,78 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using OAuthServer.Services; using OAuthServer.Services;
namespace OAuthServer.Controllers; namespace OAuthServer.Controllers;
[ApiController] [ApiController]
public class OAuthController : ControllerBase [Route("oauth")]
{ public class OAuthController : ControllerBase {
private readonly ILogger<OAuthController> _logger; private readonly ILogger<OAuthController> _logger;
private readonly JwtService _jwt; private readonly JwtService _jwt;
public OAuthController(ILogger<OAuthController> logger, JwtService jwt) public OAuthController(ILogger<OAuthController> logger, JwtService jwt) {
{
_logger = logger; _logger = logger;
_jwt = jwt; _jwt = jwt;
} }
[HttpPost] [Authorize]
[Route("get-token")] [HttpGet("authorize")]
public ActionResult GenerateToken() // ReSharper disable InconsistentNaming
{ public ActionResult Authorize(
return Ok(_jwt.GenerateToken()); [Required, Url] string redirect_uri,
string response_type,
string client_id,
string state
) {
if (string.IsNullOrEmpty(response_type) || string.IsNullOrEmpty(client_id) || string.IsNullOrEmpty(state)) {
return Redirect($"{redirect_uri}?error=invalid_request");
}
if (response_type != "code") {
return Redirect($"{redirect_uri}?error=unsupported_response_type&state={state}");
}
if (client_id != "lmao") {
return Redirect($"{redirect_uri}?error=access_denied&error_description=Invalid+client+id&state={state}");
}
// TODO: generate code
string code = Guid.NewGuid().ToString();
return Redirect($"{redirect_uri}?code={code}&state={state}");
}
public record GenerateTokenRequest(
string? grant_type,
string? code,
string? redirect_uri,
string? client_id,
string? client_secret
);
[HttpPost("token")]
[Consumes("application/x-www-form-urlencoded")]
public ActionResult GenerateToken([FromForm] GenerateTokenRequest request) {
if (string.IsNullOrEmpty(request.grant_type) || string.IsNullOrEmpty(request.code) ||
string.IsNullOrEmpty(request.redirect_uri) ||
string.IsNullOrEmpty(request.client_id)) {
return BadRequest(new {error = "invalid_request"});
}
if (request.grant_type != "authorization_code") {
return BadRequest(new {error = "unsupported_grant_type"});
}
if (request.client_id != "lmao") {
return BadRequest(new {error = "invalid_client"});
}
string token = _jwt.GenerateToken();
Response.Headers.Append("Cache-Control", "no-store");
Response.Headers.Append("Pragma", "no-cache");
return Ok(new {access_token = token, token_type = "bearer"});
} }
} }

@ -5,15 +5,12 @@ namespace OAuthServer.Controllers;
[ApiController] [ApiController]
[Route("")] [Route("")]
public class UserController : ControllerBase public class UserController : ControllerBase {
{
[HttpGet] [HttpGet]
[Authorize(Policy = "User")] [Authorize(Policy = "User")]
[Route("user")] [Route("/auth/user")]
public ActionResult GetUser() public ActionResult TestAuth()
{ {
return Ok("Authorized as User"); return Ok("Authorized as User");
} }
} }

@ -16,29 +16,23 @@ builder.Logging.AddConsole();
// Add services to the container. // Add services to the container.
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options => builder.Services.AddSwaggerGen(options => {
{
// Create a authentication schema for JWT tokens // Create a authentication schema for JWT tokens
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization", Name = "Authorization",
In = ParameterLocation.Header, In = ParameterLocation.Header,
Type = SecuritySchemeType.Http, Type = SecuritySchemeType.Http,
Scheme = "bearer", Scheme = "bearer",
Reference = new OpenApiReference Reference = new OpenApiReference {
{
Type = ReferenceType.SecurityScheme, Type = ReferenceType.SecurityScheme,
Id = "Bearer" Id = "Bearer"
} }
}); });
options.AddSecurityRequirement(new OpenApiSecurityRequirement options.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
{ {
new OpenApiSecurityScheme new OpenApiSecurityScheme {
{ Reference = new OpenApiReference {
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme, Type = ReferenceType.SecurityScheme,
Id = "Bearer" Id = "Bearer"
} }
@ -58,11 +52,9 @@ builder.Services.AddIdentity<IdentityUser, IdentityRole>(options => { options.St
var rsaKey = JwtService.GetSigningKey(); var rsaKey = JwtService.GetSigningKey();
// Add the JWT authentication method // Add the JWT authentication method
builder.Services.AddAuthentication().AddJwtBearer("OAuthToken", options => builder.Services.AddAuthentication().AddJwtBearer("OAuthToken", options => {
{
options.SaveToken = false; options.SaveToken = false;
options.TokenValidationParameters = new TokenValidationParameters() options.TokenValidationParameters = new TokenValidationParameters() {
{
ValidateIssuer = false, ValidateIssuer = false,
ValidateAudience = false, ValidateAudience = false,
RequireSignedTokens = true, RequireSignedTokens = true,
@ -70,8 +62,7 @@ builder.Services.AddAuthentication().AddJwtBearer("OAuthToken", options =>
}; };
}); });
builder.Services.Configure<IdentityOptions>(options => builder.Services.Configure<IdentityOptions>(options => {
{
// SignIn settings. // SignIn settings.
options.SignIn.RequireConfirmedAccount = false; options.SignIn.RequireConfirmedAccount = false;
options.SignIn.RequireConfirmedEmail = false; options.SignIn.RequireConfirmedEmail = false;
@ -91,8 +82,7 @@ builder.Services.Configure<IdentityOptions>(options =>
options.Password.RequiredUniqueChars = 1; options.Password.RequiredUniqueChars = 1;
}); });
builder.Services.ConfigureApplicationCookie(options => builder.Services.ConfigureApplicationCookie(options => {
{
// Cookie options // Cookie options
options.Cookie.Name = "AuthCookie"; options.Cookie.Name = "AuthCookie";
options.Cookie.HttpOnly = true; options.Cookie.HttpOnly = true;
@ -105,23 +95,20 @@ builder.Services.ConfigureApplicationCookie(options =>
}); });
// Force Identity's security stamp to be validated every minute. // Force Identity's security stamp to be validated every minute.
builder.Services.Configure<SecurityStampValidatorOptions>(options => builder.Services.Configure<SecurityStampValidatorOptions>(options => {
{
options.ValidationInterval = TimeSpan.FromMinutes(10); options.ValidationInterval = TimeSpan.FromMinutes(10);
}); });
// Set a more secure password hashing iteration count // Set a more secure password hashing iteration count
builder.Services.Configure<PasswordHasherOptions>(option => { option.IterationCount = 100_000; }); builder.Services.Configure<PasswordHasherOptions>(option => { option.IterationCount = 100_000; });
builder.Services.AddDataProtection().UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration() builder.Services.AddDataProtection().UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration() {
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC, EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256 ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
}); });
// Add policy-based authorization // Add policy-based authorization
builder.Services.AddAuthorization(options => builder.Services.AddAuthorization(options => {
{
// Require either role to authenticate as Contestant // Require either role to authenticate as Contestant
options.AddPolicy("User", policy => policy options.AddPolicy("User", policy => policy
.RequireRole("User") .RequireRole("User")
@ -140,8 +127,7 @@ builder.Services.AddSingleton<JwtService>();
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment()) {
{
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(); app.UseSwaggerUI();
} }
@ -150,10 +136,8 @@ app.MapControllers();
// Automatically apply migrations to database on startup // Automatically apply migrations to database on startup
var scopeFactory = app.Services.GetRequiredService<IServiceScopeFactory>(); var scopeFactory = app.Services.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope()) using (var scope = scopeFactory.CreateScope()) {
{ using (var databaseContext = scope.ServiceProvider.GetRequiredService<AppDbContext>()) {
using (var databaseContext = scope.ServiceProvider.GetRequiredService<AppDbContext>())
{
// Migrate the database // Migrate the database
databaseContext.Database.Migrate(); databaseContext.Database.Migrate();
} }

@ -12,9 +12,9 @@
"http": { "http": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": false,
"launchUrl": "swagger", "launchUrl": "swagger",
"applicationUrl": "http://localhost:5196", "applicationUrl": "http://localhost:1234",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

@ -5,27 +5,21 @@ using Microsoft.IdentityModel.Tokens;
namespace OAuthServer.Services; namespace OAuthServer.Services;
public class JwtService public class JwtService {
{
private readonly RSA _rsaKey; private readonly RSA _rsaKey;
public JwtService() public JwtService() {
{
_rsaKey = GetSigningKey(); _rsaKey = GetSigningKey();
} }
public static RSA GetSigningKey() public static RSA GetSigningKey() {
{
RSA rsaKey = RSA.Create(); RSA rsaKey = RSA.Create();
const string jwtKeyPath = ".aspnet/jwt-key"; const string jwtKeyPath = ".aspnet/jwt-key";
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
string fullPath = Path.Combine(home, jwtKeyPath); string fullPath = Path.Combine(home, jwtKeyPath);
if (File.Exists(fullPath)) if (File.Exists(fullPath)) {
{
rsaKey.ImportRSAPrivateKey(File.ReadAllBytes(fullPath), out _); rsaKey.ImportRSAPrivateKey(File.ReadAllBytes(fullPath), out _);
} } else {
else
{
string? dirName = Path.GetDirectoryName(fullPath); string? dirName = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(dirName)) if (!string.IsNullOrEmpty(dirName))
Directory.CreateDirectory(dirName); Directory.CreateDirectory(dirName);
@ -37,14 +31,11 @@ public class JwtService
return rsaKey; return rsaKey;
} }
public string GenerateToken() public string GenerateToken() {
{
var handler = new JsonWebTokenHandler(); var handler = new JsonWebTokenHandler();
var key = new RsaSecurityKey(_rsaKey); var key = new RsaSecurityKey(_rsaKey);
var token = handler.CreateToken(new SecurityTokenDescriptor var token = handler.CreateToken(new SecurityTokenDescriptor {
{ Subject = new ClaimsIdentity(new[] {
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.Sub, "user1"), new Claim(JwtRegisteredClaimNames.Sub, "user1"),
new Claim("role", "External"), new Claim("role", "External"),
new Claim("scope", "scope:1") new Claim("scope", "scope:1")

Loading…
Cancel
Save