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,14 +4,10 @@ using Microsoft.EntityFrameworkCore;
namespace OAuthServer;
public class AppDbContext : IdentityDbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public class AppDbContext : IdentityDbContext {
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
// Setup Identity roles

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

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

@ -1,24 +1,78 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OAuthServer.Services;
namespace OAuthServer.Controllers;
[ApiController]
public class OAuthController : ControllerBase
{
[Route("oauth")]
public class OAuthController : ControllerBase {
private readonly ILogger<OAuthController> _logger;
private readonly JwtService _jwt;
public OAuthController(ILogger<OAuthController> logger, JwtService jwt)
{
public OAuthController(ILogger<OAuthController> logger, JwtService jwt) {
_logger = logger;
_jwt = jwt;
}
[HttpPost]
[Route("get-token")]
public ActionResult GenerateToken()
{
return Ok(_jwt.GenerateToken());
[Authorize]
[HttpGet("authorize")]
// ReSharper disable InconsistentNaming
public ActionResult Authorize(
[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]
[Route("")]
public class UserController : ControllerBase
{
public class UserController : ControllerBase {
[HttpGet]
[Authorize(Policy = "User")]
[Route("user")]
public ActionResult GetUser()
[Route("/auth/user")]
public ActionResult TestAuth()
{
return Ok("Authorized as User");
}
}

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

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

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

Loading…
Cancel
Save