diff --git a/.idea/.idea.OAuthServer/.idea/dataSources.xml b/.idea/.idea.OAuthServer/.idea/dataSources.xml
new file mode 100644
index 0000000..f96e37b
--- /dev/null
+++ b/.idea/.idea.OAuthServer/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ sqlite.xerial
+ true
+ org.sqlite.JDBC
+ jdbc:sqlite:$PROJECT_DIR$/OAuthServer/db.sqlite3
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.OAuthServer/.idea/sqldialects.xml b/.idea/.idea.OAuthServer/.idea/sqldialects.xml
new file mode 100644
index 0000000..c0e01ca
--- /dev/null
+++ b/.idea/.idea.OAuthServer/.idea/sqldialects.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OAuthServer.sln.DotSettings.user b/OAuthServer.sln.DotSettings.user
new file mode 100644
index 0000000..e9e707c
--- /dev/null
+++ b/OAuthServer.sln.DotSettings.user
@@ -0,0 +1,2 @@
+
+ ShowAndRun
\ No newline at end of file
diff --git a/OAuthServer/AppDbContext.cs b/OAuthServer/AppDbContext.cs
index 6ca8802..0480ccb 100644
--- a/OAuthServer/AppDbContext.cs
+++ b/OAuthServer/AppDbContext.cs
@@ -4,20 +4,16 @@ using Microsoft.EntityFrameworkCore;
namespace OAuthServer;
-public class AppDbContext : IdentityDbContext
-{
- public AppDbContext(DbContextOptions options) : base(options)
- {
- }
+public class AppDbContext : IdentityDbContext {
+ public AppDbContext(DbContextOptions options) : base(options) { }
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
+ protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
// Setup Identity roles
modelBuilder.Entity().HasData(
- 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 = "User", NormalizedName = "USER"},
+ new IdentityRole {Id = Guid.NewGuid().ToString(), Name = "External", NormalizedName = "EXTERNAL"}
);
}
}
\ No newline at end of file
diff --git a/OAuthServer/Controllers/ExternalController.cs b/OAuthServer/Controllers/ExternalController.cs
index af583f0..5f559f4 100644
--- a/OAuthServer/Controllers/ExternalController.cs
+++ b/OAuthServer/Controllers/ExternalController.cs
@@ -6,27 +6,30 @@ namespace OAuthServer.Controllers;
[ApiController]
[Route("")]
-public class ExternalController : ControllerBase
-{
+public class ExternalController : ControllerBase {
private readonly ILogger _logger;
- public ExternalController(ILogger logger)
- {
+ public ExternalController(ILogger 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});
+ }
}
\ No newline at end of file
diff --git a/OAuthServer/Controllers/LoginController.cs b/OAuthServer/Controllers/LoginController.cs
index 2ec1c9d..d4caa13 100644
--- a/OAuthServer/Controllers/LoginController.cs
+++ b/OAuthServer/Controllers/LoginController.cs
@@ -7,13 +7,11 @@ namespace OAuthServer.Controllers;
[ApiController]
[Route("")]
-public class LoginController : ControllerBase
-{
+public class LoginController : ControllerBase {
private readonly SignInManager _signInManager;
private readonly UserManager _userManager;
- public LoginController(SignInManager signInManager, UserManager userManager)
- {
+ public LoginController(SignInManager signInManager, UserManager userManager) {
_signInManager = signInManager;
_userManager = userManager;
}
@@ -22,22 +20,18 @@ public class LoginController : ControllerBase
[HttpPost]
[Route("register")]
- public async Task> Register([FromBody] RegisterRequest registerRequest)
- {
- IdentityUser user = new IdentityUser
- {
+ public async Task> 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("""
@@ -72,18 +65,15 @@ public class LoginController : ControllerBase
[HttpPost]
[Route("login")]
- public async Task Login([FromForm] LoginRequest loginRequest, string? returnUrl)
- {
+ public async Task 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 Logout()
- {
+ public async Task Logout() {
await _signInManager.SignOutAsync();
return Ok("Successfully logged out");
}
diff --git a/OAuthServer/Controllers/OAuthController.cs b/OAuthServer/Controllers/OAuthController.cs
index 6c5141a..9a67766 100644
--- a/OAuthServer/Controllers/OAuthController.cs
+++ b/OAuthServer/Controllers/OAuthController.cs
@@ -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 _logger;
private readonly JwtService _jwt;
- public OAuthController(ILogger logger, JwtService jwt)
- {
+ public OAuthController(ILogger 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"});
}
}
\ No newline at end of file
diff --git a/OAuthServer/Controllers/UserController.cs b/OAuthServer/Controllers/UserController.cs
index 01ce915..ab7f012 100644
--- a/OAuthServer/Controllers/UserController.cs
+++ b/OAuthServer/Controllers/UserController.cs
@@ -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");
}
}
\ No newline at end of file
diff --git a/OAuthServer/Program.cs b/OAuthServer/Program.cs
index 3286fa9..c3d57ac 100644
--- a/OAuthServer/Program.cs
+++ b/OAuthServer/Program.cs
@@ -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
- {
+ options.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
- new OpenApiSecurityScheme
- {
- Reference = new OpenApiReference
- {
+ new OpenApiSecurityScheme {
+ Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
@@ -58,11 +52,9 @@ builder.Services.AddIdentity(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(options =>
-{
+builder.Services.Configure(options => {
// SignIn settings.
options.SignIn.RequireConfirmedAccount = false;
options.SignIn.RequireConfirmedEmail = false;
@@ -91,8 +82,7 @@ builder.Services.Configure(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(options =>
-{
+builder.Services.Configure(options => {
options.ValidationInterval = TimeSpan.FromMinutes(10);
});
// Set a more secure password hashing iteration count
builder.Services.Configure(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();
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();
-using (var scope = scopeFactory.CreateScope())
-{
- using (var databaseContext = scope.ServiceProvider.GetRequiredService())
- {
+using (var scope = scopeFactory.CreateScope()) {
+ using (var databaseContext = scope.ServiceProvider.GetRequiredService()) {
// Migrate the database
databaseContext.Database.Migrate();
}
diff --git a/OAuthServer/Properties/launchSettings.json b/OAuthServer/Properties/launchSettings.json
index 72a93d3..8b4801f 100644
--- a/OAuthServer/Properties/launchSettings.json
+++ b/OAuthServer/Properties/launchSettings.json
@@ -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"
}
diff --git a/OAuthServer/Services/JwtService.cs b/OAuthServer/Services/JwtService.cs
index 3b36d15..1c00955 100644
--- a/OAuthServer/Services/JwtService.cs
+++ b/OAuthServer/Services/JwtService.cs
@@ -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")