Started implementing oauth2 based on MoonCore helper services

Its more or less a test how well the helper services improve the implementation. I havent implemented anything fancy here atm. Just testing the oauth2 flow
This commit is contained in:
Masu Baumgartner
2024-10-18 00:03:20 +02:00
parent 13daa3cbac
commit 9d1351527d
13 changed files with 513 additions and 178 deletions

View File

@@ -1,7 +1,11 @@
namespace Moonlight.ApiServer.Configuration; using MoonCore.Helpers;
namespace Moonlight.ApiServer.Configuration;
public class AppConfiguration public class AppConfiguration
{ {
public string PublicUrl { get; set; } = "http://localhost:5165";
public DatabaseConfig Database { get; set; } = new(); public DatabaseConfig Database { get; set; } = new();
public AuthenticationConfig Authentication { get; set; } = new(); public AuthenticationConfig Authentication { get; set; } = new();
public DevelopmentConfig Development { get; set; } = new(); public DevelopmentConfig Development { get; set; } = new();
@@ -19,12 +23,25 @@ public class AppConfiguration
public class AuthenticationConfig public class AuthenticationConfig
{ {
public string Secret { get; set; } = Guid public string MlAccessSecret { get; set; } = Formatter.GenerateString(32);
.NewGuid() public string MlRefreshSecret { get; set; } = Formatter.GenerateString(32);
.ToString()
.Replace("-", ""); public string Secret { get; set; } = Formatter.GenerateString(32);
public int TokenDuration { get; set; } = 10; public int TokenDuration { get; set; } = 10;
public bool UseLocalOAuth2Service { get; set; } = true;
public string AccessSecret { get; set; } = Formatter.GenerateString(32);
public string RefreshSecret { get; set; } = Formatter.GenerateString(32);
public string ClientId { get; set; } = Formatter.GenerateString(8);
public string ClientSecret { get; set; } = Formatter.GenerateString(32);
public string? AuthorizationUri { get; set; }
public string? AuthorizationRedirect { get; set; }
public string? AccessEndpoint { get; set; }
public string? RefreshEndpoint { get; set; }
// Local OAuth2 Service
public string CodeSecret { get; set; } = Formatter.GenerateString(32);
} }
public class DevelopmentConfig public class DevelopmentConfig

View File

@@ -10,4 +10,8 @@ public class User
public DateTime TokenValidTimestamp { get; set; } = DateTime.UtcNow; public DateTime TokenValidTimestamp { get; set; } = DateTime.UtcNow;
public string PermissionsJson { get; set; } = "[]"; public string PermissionsJson { get; set; } = "[]";
public string AccessToken { get; set; } = "";
public string RefreshToken { get; set; } = "";
public DateTime RefreshTimestamp { get; set; } = DateTime.UtcNow;
} }

View File

@@ -0,0 +1,74 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.ApiServer.Database;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
[DbContext(typeof(CoreDataContext))]
[Migration("20241017214600_AddedAccessAndRefreshTokenFields")]
partial class AddedAccessAndRefreshTokenFields
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Core")
.HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("RefreshTimestamp")
.HasColumnType("datetime(6)");
b.Property<string>("RefreshToken")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("datetime(6)");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Users", "Core");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedAccessAndRefreshTokenFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "AccessToken",
schema: "Core",
table: "Users",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<DateTime>(
name: "RefreshTimestamp",
schema: "Core",
table: "Users",
type: "datetime(6)",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.AddColumn<string>(
name: "RefreshToken",
schema: "Core",
table: "Users",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AccessToken",
schema: "Core",
table: "Users");
migrationBuilder.DropColumn(
name: "RefreshTimestamp",
schema: "Core",
table: "Users");
migrationBuilder.DropColumn(
name: "RefreshToken",
schema: "Core",
table: "Users");
}
}
}

View File

@@ -31,6 +31,10 @@ namespace Moonlight.ApiServer.Database.Migrations
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id")); MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Email") b.Property<string>("Email")
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
@@ -43,6 +47,13 @@ namespace Moonlight.ApiServer.Database.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<DateTime>("RefreshTimestamp")
.HasColumnType("datetime(6)");
b.Property<string>("RefreshToken")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("TokenValidTimestamp") b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");

View File

@@ -1,5 +1,12 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Extended.OAuth2.ApiServer;
using MoonCore.Helpers;
using MoonCore.Services;
using Moonlight.ApiServer.Attributes; using Moonlight.ApiServer.Attributes;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Helpers.Authentication; using Moonlight.ApiServer.Helpers.Authentication;
using Moonlight.ApiServer.Services; using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Requests.Auth; using Moonlight.Shared.Http.Requests.Auth;
@@ -11,37 +18,56 @@ namespace Moonlight.ApiServer.Http.Controllers.Auth;
[Route("api/auth")] [Route("api/auth")]
public class AuthController : Controller public class AuthController : Controller
{ {
private readonly AuthService AuthService; private readonly OAuth2Service OAuth2Service;
private readonly TokenHelper TokenHelper;
private readonly ConfigService<AppConfiguration> ConfigService;
private readonly DatabaseRepository<User> UserRepository;
public AuthController(AuthService authService) public AuthController(OAuth2Service oAuth2Service, TokenHelper tokenHelper, DatabaseRepository<User> userRepository, ConfigService<AppConfiguration> configService)
{ {
AuthService = authService; OAuth2Service = oAuth2Service;
TokenHelper = tokenHelper;
UserRepository = userRepository;
ConfigService = configService;
} }
[HttpPost("login")] [HttpGet("start")]
public async Task<LoginResponse> Login([FromBody] LoginRequest request) public async Task<AuthStartResponse> Start()
{ {
var user = await AuthService.Login(request.Email, request.Password); var data = await OAuth2Service.StartAuthorizing();
return new LoginResponse() return Mapper.Map<AuthStartResponse>(data);
{
Token = await AuthService.GenerateToken(user)
};
} }
[HttpPost("register")] [HttpGet("handle")]
public async Task<RegisterResponse> Register([FromBody] RegisterRequest request) public async Task Handle([FromQuery(Name = "code")] string code)
{ {
var user = await AuthService.Register( //TODO: Validate jwt syntax
request.Username,
request.Email,
request.Password
);
return new RegisterResponse() var accessData = await OAuth2Service.RequestAccess(code);
//TODO: Add modular oauth2 consumer system
var userId = 1;
var user = UserRepository.Get().First(x => x.Id == userId);
user.AccessToken = accessData.AccessToken;
user.RefreshToken = accessData.RefreshToken;
user.RefreshTimestamp = DateTime.UtcNow.AddSeconds(accessData.ExpiresIn);
UserRepository.Update(user);
var authConfig = ConfigService.Get().Authentication;
var tokenPair = await TokenHelper.GeneratePair(authConfig.MlAccessSecret, authConfig.MlAccessSecret, data =>
{ {
Token = await AuthService.GenerateToken(user) data.Add("userId", user.Id.ToString());
}; });
Response.Cookies.Append("ml-access", tokenPair.AccessToken);
Response.Cookies.Append("ml-refresh", tokenPair.RefreshToken);
Response.Cookies.Append("ml-timestamp", DateTimeOffset.UtcNow.AddSeconds(3600).ToUnixTimeSeconds().ToString());
Response.Redirect("/");
} }
[HttpGet("check")] [HttpGet("check")]

View File

@@ -0,0 +1,133 @@
using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.OAuth2.AuthServer;
using MoonCore.Extended.OAuth2.Models;
using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Services;
namespace Moonlight.ApiServer.Http.Controllers.OAuth2;
[ApiController]
[Route("oauth2")]
public class OAuth2Controller : Controller
{
private readonly OAuth2Service OAuth2Service;
private readonly AuthService AuthService;
private readonly DatabaseRepository<User> UserRepository;
private readonly ConfigService<AppConfiguration> ConfigService;
public OAuth2Controller(OAuth2Service oAuth2Service, ConfigService<AppConfiguration> configService,
AuthService authService, DatabaseRepository<User> userRepository)
{
OAuth2Service = oAuth2Service;
ConfigService = configService;
AuthService = authService;
UserRepository = userRepository;
}
[HttpGet("authorize")]
public async Task Authorize(
[FromQuery(Name = "response_type")] string responseType,
[FromQuery(Name = "client_id")] string clientId,
[FromQuery(Name = "redirect_uri")] string redirectUri
)
{
if (responseType != "code")
throw new HttpApiException("Invalid response type", 400);
var config = ConfigService.Get();
// TODO: This call should be handled by the OAuth2Service
if (clientId != config.Authentication.ClientId)
throw new HttpApiException("Invalid client id", 400);
if (redirectUri != (config.Authentication.AuthorizationRedirect ?? $"{config.PublicUrl}/api/auth/handle"))
throw new HttpApiException("Invalid redirect uri", 400);
Response.StatusCode = 200;
await Response.WriteAsync(
"<h1>Login lol</h1><br />" +
"<br />" +
"<br />" +
"<form method=\"post\">" +
"<label for=\"email\">Email:</label>" +
"<input type=\"email\" id=\"email\" name=\"email\"><br>" +
"<br>" +
"<label for=\"password\">Password:</label>" +
"<input type=\"password\" id=\"password\" name=\"password\"><br>" +
"<br>" +
"<input type=\"submit\" value=\"Submit\">" +
"</form>"
);
}
[HttpPost("authorize")]
public async Task AuthorizePost(
[FromQuery(Name = "response_type")] string responseType,
[FromQuery(Name = "client_id")] string clientId,
[FromQuery(Name = "redirect_uri")] string redirectUri,
[FromForm(Name = "email")] string email,
[FromForm(Name = "password")] string password
)
{
if (responseType != "code")
throw new HttpApiException("Invalid response type", 400);
var config = ConfigService.Get();
// TODO: This call should be handled by the OAuth2Service
if (clientId != config.Authentication.ClientId)
throw new HttpApiException("Invalid client id", 400);
if (redirectUri != (config.Authentication.AuthorizationRedirect ?? $"{config.PublicUrl}/api/auth/handle"))
throw new HttpApiException("Invalid redirect uri", 400);
var user = await AuthService.Login(email, password);
var code = await OAuth2Service.GenerateCode(data => { data.Add("userId", user.Id.ToString()); });
var redirectUrl = redirectUri +
$"?code={code}";
Response.Redirect(redirectUrl);
}
[HttpPost("access")]
public async Task<AccessData> Access(
[FromForm(Name = "client_id")] string clientId,
[FromForm(Name = "client_secret")] string clientSecret,
[FromForm(Name = "redirect_uri")] string redirectUri,
[FromForm(Name = "grant_type")] string grantType,
[FromForm(Name = "code")] string code
)
{
if (grantType != "authorization_code")
throw new HttpApiException("Invalid grant type", 400);
User? user = null;
var access = await OAuth2Service.ValidateAccess(clientId, clientSecret, redirectUri, code, data =>
{
if (!data.TryGetValue("userId", out var userIdStr))
return false;
if (!int.TryParse(userIdStr, out var userId))
return false;
user = UserRepository.Get().FirstOrDefault(x => x.Id == userId);
return user != null;
}, data =>
{
data.Add("userId", user!.Id.ToString());
});
if (access == null)
throw new HttpApiException("Unable to validate access", 400);
return access;
}
}

View File

@@ -1,6 +1,8 @@
using System.Text.Json; using System.Text.Json;
using MoonCore.Extended.Abstractions; using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers; using MoonCore.Extended.Helpers;
using MoonCore.Extended.Models;
using MoonCore.Extended.OAuth2.ApiServer;
using MoonCore.Services; using MoonCore.Services;
using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Database.Entities;
@@ -28,6 +30,82 @@ public class AuthenticationMiddleware
private async Task Authenticate(HttpContext context) private async Task Authenticate(HttpContext context)
{ {
var request = context.Request; var request = context.Request;
if (!request.Cookies.TryGetValue("ml-access", out var accessToken) ||
!request.Cookies.TryGetValue("ml-refresh", out var refreshToken))
return;
if(string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
return;
// TODO: Validate if both are valid jwts (maybe)
//
var tokenHelper = context.RequestServices.GetRequiredService<TokenHelper>();
var configService = context.RequestServices.GetRequiredService<ConfigService<AppConfiguration>>();
User? user = null;
if (!await tokenHelper.IsValidAccessToken(accessToken, configService.Get().Authentication.MlAccessSecret,
data =>
{
if (!data.TryGetValue("userId", out var userIdStr))
return false;
if (!int.TryParse(userIdStr, out var userId))
return false;
var userRepo = context.RequestServices.GetRequiredService<DatabaseRepository<User>>();
user = userRepo.Get().FirstOrDefault(x => x.Id == userId);
return user != null;
}))
{
return;
}
if(user == null)
return;
// Validate external access
if (DateTime.UtcNow > user.RefreshTimestamp)
{
var tokenConsumer = new TokenConsumer(user.AccessToken, user.RefreshToken, user.RefreshTimestamp,
async refreshToken =>
{
var oauth2Service = context.RequestServices.GetRequiredService<OAuth2Service>();
var accessData = await oauth2Service.RefreshAccess(refreshToken);
user.AccessToken = accessData.AccessToken;
user.RefreshToken = accessData.RefreshToken;
user.RefreshTimestamp = DateTime.UtcNow.AddSeconds(accessData.ExpiresIn);
var userRepo = context.RequestServices.GetRequiredService<DatabaseRepository<User>>();
userRepo.Update(user);
return new TokenPair()
{
AccessToken = user.AccessToken,
RefreshToken = user.RefreshToken
};
});
await tokenConsumer.GetAccessToken();
//TODO: API CALL
}
// Load permissions, handle empty values
var permissions = JsonSerializer.Deserialize<string[]>(
string.IsNullOrEmpty(user.PermissionsJson) ? "[]" : user.PermissionsJson
) ?? [];
// Save permission state
context.User = new PermClaimsPrinciple(permissions, user);
/*
string? token = null; string? token = null;
// Cookie for Moonlight.Client // Cookie for Moonlight.Client
@@ -62,7 +140,7 @@ public class AuthenticationMiddleware
if (token.Count(x => x == '.') == 2) // JWT only has two dots if (token.Count(x => x == '.') == 2) // JWT only has two dots
await AuthenticateUser(context, token); await AuthenticateUser(context, token);
else else
await AuthenticateApiKey(context, token); await AuthenticateApiKey(context, token);*/
} }
private async Task AuthenticateUser(HttpContext context, string jwt) private async Task AuthenticateUser(HttpContext context, string jwt)
@@ -71,12 +149,12 @@ public class AuthenticationMiddleware
var configService = context.RequestServices.GetRequiredService<ConfigService<AppConfiguration>>(); var configService = context.RequestServices.GetRequiredService<ConfigService<AppConfiguration>>();
var secret = configService.Get().Authentication.Secret; var secret = configService.Get().Authentication.Secret;
if(!await jwtHelper.Validate(secret, jwt, "login")) if (!await jwtHelper.Validate(secret, jwt, "login"))
return; return;
var data = await jwtHelper.Decode(secret, jwt); var data = await jwtHelper.Decode(secret, jwt);
if(!data.TryGetValue("iat", out var issuedAtString) || !data.TryGetValue("userId", out var userIdString)) if (!data.TryGetValue("iat", out var issuedAtString) || !data.TryGetValue("userId", out var userIdString))
return; return;
var userId = int.Parse(userIdString); var userId = int.Parse(userIdString);
@@ -85,11 +163,11 @@ public class AuthenticationMiddleware
var userRepo = context.RequestServices.GetRequiredService<DatabaseRepository<User>>(); var userRepo = context.RequestServices.GetRequiredService<DatabaseRepository<User>>();
var user = userRepo.Get().FirstOrDefault(x => x.Id == userId); var user = userRepo.Get().FirstOrDefault(x => x.Id == userId);
if(user == null) if (user == null)
return; return;
// Check if token is in the past // Check if token is in the past
if(user.TokenValidTimestamp > issuedAt) if (user.TokenValidTimestamp > issuedAt)
return; return;
// Load permissions, handle empty values // Load permissions, handle empty values
@@ -103,6 +181,5 @@ public class AuthenticationMiddleware
private async Task AuthenticateApiKey(HttpContext context, string apiKey) private async Task AuthenticateApiKey(HttpContext context, string apiKey)
{ {
} }
} }

View File

@@ -12,13 +12,13 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MoonCore" Version="1.5.8" /> <PackageReference Include="MoonCore" Version="1.5.9" />
<PackageReference Include="MoonCore.Extended" Version="1.0.7" /> <PackageReference Include="MoonCore.Extended" Version="1.0.8" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.0" /> <PackageReference Include="MoonCore.PluginFramework" Version="1.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
<PackageReference Include="Ben.Demystifier" Version="0.4.1" /> <PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.0.5" /> <PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.0.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,5 +1,6 @@
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using MoonCore.Extended.Abstractions; using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Extensions;
using MoonCore.Extended.Helpers; using MoonCore.Extended.Helpers;
using MoonCore.Extensions; using MoonCore.Extensions;
using MoonCore.Helpers; using MoonCore.Helpers;
@@ -20,6 +21,8 @@ var configService = new ConfigService<AppConfiguration>(
PathBuilder.File("storage", "config.json") PathBuilder.File("storage", "config.json")
); );
var config = configService.Get();
ApplicationStateHelper.SetConfiguration(configService); ApplicationStateHelper.SetConfiguration(configService);
// Build pre run logger // Build pre run logger
@@ -79,6 +82,52 @@ builder.Services.AddSingleton(configService);
builder.Services.AddSingleton<JwtHelper>(); builder.Services.AddSingleton<JwtHelper>();
builder.Services.AutoAddServices<Program>(); builder.Services.AutoAddServices<Program>();
// OAuth2
builder.Services.AddSingleton<TokenHelper>();
builder.Services.AddHttpClient();
builder.Services.AddOAuth2Consumer(configuration =>
{
configuration.ClientId = config.Authentication.ClientId;
configuration.ClientSecret = config.Authentication.ClientSecret;
configuration.AuthorizationRedirect =
config.Authentication.AuthorizationRedirect ?? $"{config.PublicUrl}/api/auth/handle";
configuration.AccessEndpoint = config.Authentication.AccessEndpoint ?? $"{config.PublicUrl}/oauth2/access";
configuration.RefreshEndpoint = config.Authentication.RefreshEndpoint ?? $"{config.PublicUrl}/oauth2/refresh";
if (config.Authentication.UseLocalOAuth2Service)
{
configuration.AuthorizationEndpoint = config.Authentication.AuthorizationRedirect ?? $"{config.PublicUrl}/oauth2/authorize";
}
else
{
if(config.Authentication.AuthorizationUri == null)
logger.LogWarning("The 'AuthorizationUri' for the oauth2 client is not set. If you want to use an external oauth2 provider, you need to specify this url. If you want to use the local oauth2 service, set 'UseLocalOAuth2Service' to true");
configuration.AuthorizationEndpoint = config.Authentication.AuthorizationUri!;
}
});
if (config.Authentication.UseLocalOAuth2Service)
{
logger.LogInformation("Using local oauth2 provider");
builder.Services.AddOAuth2Provider(configuration =>
{
configuration.AccessSecret = config.Authentication.AccessSecret;
configuration.RefreshSecret = config.Authentication.RefreshSecret;
configuration.ClientId = config.Authentication.ClientId;
configuration.ClientId = config.Authentication.ClientSecret;
configuration.CodeSecret = config.Authentication.CodeSecret;
configuration.AuthorizationRedirect =
config.Authentication.AuthorizationRedirect ?? $"{config.PublicUrl}/api/auth/handle";
configuration.AccessTokenDuration = 60;
configuration.RefreshTokenDuration = 3600;
});
}
// Database // Database
var databaseHelper = new DatabaseHelper( var databaseHelper = new DatabaseHelper(
loggerFactory.CreateLogger<DatabaseHelper>() loggerFactory.CreateLogger<DatabaseHelper>()
@@ -110,7 +159,7 @@ using (var scope = app.Services.CreateScope())
await databaseHelper.EnsureMigrated(scope.ServiceProvider); await databaseHelper.EnsureMigrated(scope.ServiceProvider);
} }
if(app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
app.UseWebAssemblyDebugging(); app.UseWebAssemblyDebugging();
app.UseBlazorFrameworkFiles(); app.UseBlazorFrameworkFiles();

View File

@@ -10,9 +10,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.6" PrivateAssets="all"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.6" PrivateAssets="all"/>
<PackageReference Include="MoonCore" Version="1.5.8" /> <PackageReference Include="MoonCore" Version="1.5.9" />
<PackageReference Include="MoonCore.Blazor" Version="1.2.1" /> <PackageReference Include="MoonCore.Blazor" Version="1.2.1" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.0" /> <PackageReference Include="MoonCore.PluginFramework" Version="1.0.0" />
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.0.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -29,10 +30,4 @@
<Folder Include="wwwroot\css\" /> <Folder Include="wwwroot\css\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="MoonCore.Blazor.Tailwind">
<HintPath>..\..\..\..\GitHub\Marcel-Baumgartner\MoonCore\MoonCore\MoonCore.Blazor.Tailwind\bin\Debug\net8.0\MoonCore.Blazor.Tailwind.dll</HintPath>
</Reference>
</ItemGroup>
</Project> </Project>

View File

@@ -1,141 +1,24 @@
@page "/auth/register"
@page "/auth/login"
@using MoonCore.Blazor.Tailwind.Forms.Components
@using MoonCore.Helpers @using MoonCore.Helpers
@using Moonlight.Client.Services
@using Moonlight.Client.UI.Layouts
@using Moonlight.Shared.Http.Requests.Auth
@using Moonlight.Shared.Http.Responses.Auth @using Moonlight.Shared.Http.Responses.Auth
@implements IDisposable
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject HttpApiClient ApiClient @inject HttpApiClient HttpApiClient
@inject IdentityService IdentityService
@{
var url = new Uri(Navigation.Uri);
var isRegister = url.LocalPath.StartsWith("/auth/register");
}
<div class="h-full w-full min-h-[100dvh] flex items-center justify-center px-5 md:px-0">
@if (isRegister)
{
<div class="card card-body w-full max-w-lg">
<div class="sm:mx-auto sm:w-full sm:max-w-sm mb-5">
<img class="mx-auto h-16 w-auto" src="/logolong.webp" alt="Moonlight">
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-100">Register your account</h2>
</div>
<HandleForm @ref="RegisterForm" Model="RegisterRequest" OnValidSubmit="OnSubmitRegister">
<GeneratedForm Model="RegisterRequest" OnConfigure="OnConfigureRegister" Gap="gap-x-6 gap-y-3"/>
</HandleForm>
<div class="mt-5 flex flex-col justify-center">
<WButton OnClick="_ => RegisterForm.Submit()" CssClasses="btn btn-primary">Register</WButton>
<p class="mt-3 text-center text-sm text-gray-500">
Already registered?
<a href="/auth/login" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Login</a>
</p>
</div>
</div>
}
else
{
<div class="card card-body w-full max-w-lg">
<div class="sm:mx-auto sm:w-full sm:max-w-sm mb-5">
<img class="mx-auto h-16 w-auto" src="/logolong.webp" alt="Moonlight">
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-100">Sign in to your account</h2>
</div>
<HandleForm @ref="LoginForm" Model="LoginRequest" OnValidSubmit="OnSubmitLogin">
<GeneratedForm Model="LoginRequest" OnConfigure="OnConfigureLogin" Gap="gap-x-6 gap-y-3"/>
</HandleForm>
<div class="mt-5 flex flex-col justify-center">
<WButton OnClick="_ => LoginForm.Submit()" CssClasses="btn btn-primary">Login</WButton>
<p class="mt-3 text-center text-sm text-gray-500">
Need an account registered?
<a href="/auth/register" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Register</a>
</p>
</div>
</div>
}
<div class="flex justify-center">
<WButton OnClick="StartAuth" CssClasses="btn btn-primary">Authenticate</WButton>
</div> </div>
@code @code
{ {
[CascadingParameter] public MainLayout Layout { get; set; } private async Task StartAuth(WButton _)
// Page change handling
protected override void OnInitialized()
{ {
Navigation.LocationChanged += NavigationOnLocationChanged; var authStartData = await HttpApiClient.GetJson<AuthStartResponse>("api/auth/start");
}
private async void NavigationOnLocationChanged(object? sender, LocationChangedEventArgs e) var uri = authStartData.Endpoint
=> await InvokeAsync(StateHasChanged); + $"?client_id={authStartData.ClientId}" +
$"&redirect_uri={authStartData.RedirectUri}" +
$"&response_type=code";
public void Dispose() Navigation.NavigateTo(uri, true);
{
Navigation.LocationChanged -= NavigationOnLocationChanged;
}
// Register
private RegisterRequest RegisterRequest = new();
private HandleForm RegisterForm;
private async Task OnSubmitRegister()
{
var response = await ApiClient.PostJson<RegisterResponse>("api/auth/register", RegisterRequest);
await IdentityService.Login(response.Token);
await HandleAfterAuthPage();
}
private void OnConfigureRegister(FormConfiguration<RegisterRequest> configuration)
{
configuration.WithField(x => x.Username, fieldConfiguration => { fieldConfiguration.Columns = 6; });
configuration.WithField(x => x.Email, fieldConfiguration => { fieldConfiguration.Columns = 6; });
configuration
.WithField(x => x.Password, fieldConfiguration => { fieldConfiguration.Columns = 6; })
.WithComponent<StringComponent>(component => { component.Type = "password"; });
}
// Login
private LoginRequest LoginRequest = new();
private HandleForm LoginForm;
private async Task OnSubmitLogin()
{
var response = await ApiClient.PostJson<LoginResponse>("api/auth/login", LoginRequest);
await IdentityService.Login(response.Token);
await HandleAfterAuthPage();
}
private void OnConfigureLogin(FormConfiguration<LoginRequest> configuration)
{
configuration.WithField(x => x.Email, fieldConfiguration => { fieldConfiguration.Columns = 6; });
configuration
.WithField(x => x.Password, fieldConfiguration => { fieldConfiguration.Columns = 6; })
.WithComponent<StringComponent>(component => { component.Type = "password"; });
}
// Navigation handling
private async Task HandleAfterAuthPage()
{
var url = new Uri(Navigation.Uri);
if (url.LocalPath.StartsWith("/auth/login") || url.LocalPath.StartsWith("/auth/register"))
Navigation.NavigateTo("/");
await Layout.Load();
} }
} }

View File

@@ -0,0 +1,8 @@
namespace Moonlight.Shared.Http.Responses.Auth;
public class AuthStartResponse
{
public string Endpoint { get; set; }
public string ClientId { get; set; }
public string RedirectUri { get; set; }
}