diff --git a/Moonlight.ApiServer/Configuration/AppConfiguration.cs b/Moonlight.ApiServer/Configuration/AppConfiguration.cs index 3691cfb2..07232307 100644 --- a/Moonlight.ApiServer/Configuration/AppConfiguration.cs +++ b/Moonlight.ApiServer/Configuration/AppConfiguration.cs @@ -54,26 +54,18 @@ public record AppConfiguration { [YamlMember(Description = "The secret token to use for creating jwts and encrypting things. This needs to be at least 32 characters long")] public string Secret { get; set; } = Formatter.GenerateString(32); - - [YamlMember(Description = "The lifespan of generated user tokens in hours")] - public int TokenDuration { get; set; } = 24 * 10; - [YamlMember(Description = "This enables the use of the local oauth2 provider, so moonlight will use itself as an oauth2 provider")] - public bool EnableLocalOAuth2 { get; set; } = true; - public OAuth2Data OAuth2 { get; set; } = new(); + [YamlMember(Description = "Settings for the user sessions")] + public SessionsConfig Sessions { get; set; } = new(); - public record OAuth2Data - { - public string Secret { get; set; } = Formatter.GenerateString(32); - public string ClientId { get; set; } = Formatter.GenerateString(8); - public string ClientSecret { get; set; } = Formatter.GenerateString(32); - public string? AuthorizationEndpoint { get; set; } - public string? AccessEndpoint { get; set; } - public string? AuthorizationRedirect { get; set; } + [YamlMember(Description = "This specifies if the first registered/synced user will become an admin automatically")] + public bool FirstUserAdmin { get; set; } = true; + } - [YamlMember(Description = "This specifies if the first registered user will become an admin automatically. This only works when using local oauth2")] - public bool FirstUserAdmin { get; set; } = true; - } + public record SessionsConfig + { + public string CookieName { get; set; } = "session"; + public int ExpiresIn { get; set; } = 10; } public record DevelopmentConfig diff --git a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs index ddef78fb..38f9fb6a 100644 --- a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs @@ -1,16 +1,9 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; -using MoonCore.Exceptions; -using MoonCore.Extended.Abstractions; -using Moonlight.ApiServer.Configuration; -using Moonlight.ApiServer.Database.Entities; -using Moonlight.ApiServer.Interfaces; -using Moonlight.Shared.Http.Requests.Auth; +using Moonlight.ApiServer.Implementations.LocalAuth; using Moonlight.Shared.Http.Responses.Auth; namespace Moonlight.ApiServer.Http.Controllers.Auth; @@ -19,93 +12,87 @@ namespace Moonlight.ApiServer.Http.Controllers.Auth; [Route("api/auth")] public class AuthController : Controller { - private readonly AppConfiguration Configuration; - private readonly DatabaseRepository UserRepository; - private readonly IOAuth2Provider OAuth2Provider; + private readonly IAuthenticationSchemeProvider SchemeProvider; - public AuthController( - AppConfiguration configuration, - DatabaseRepository userRepository, - IOAuth2Provider oAuth2Provider - ) + // Add schemes which should be offered to the client here + private readonly string[] SchemeWhitelist = [LocalAuthConstants.AuthenticationScheme]; + + public AuthController(IAuthenticationSchemeProvider schemeProvider) { - UserRepository = userRepository; - OAuth2Provider = oAuth2Provider; - Configuration = configuration; + SchemeProvider = schemeProvider; } - [AllowAnonymous] - [HttpGet("start")] - public async Task Start() + [HttpGet] + public async Task GetSchemes() { - var url = await OAuth2Provider.Start(); + var schemes = await SchemeProvider.GetAllSchemesAsync(); - return new LoginStartResponse() - { - Url = url - }; - } - - [AllowAnonymous] - [HttpPost("complete")] - public async Task Complete([FromBody] LoginCompleteRequest request) - { - var user = await OAuth2Provider.Complete(request.Code); - - if (user == null) - throw new HttpApiException("Unable to load user data", 500); - - // Generate token - var securityTokenDescriptor = new SecurityTokenDescriptor() - { - Expires = DateTime.Now.AddHours(Configuration.Authentication.TokenDuration), - IssuedAt = DateTime.Now, - NotBefore = DateTime.Now.AddMinutes(-1), - Claims = new Dictionary() + return schemes + .Where(x => SchemeWhitelist.Contains(x.Name)) + .Select(scheme => new AuthSchemeResponse() { - { - "userId", - user.Id - }, - { - "permissions", - string.Join(";", user.Permissions) - } - }, - SigningCredentials = new SigningCredentials( - new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(Configuration.Authentication.Secret) - ), - SecurityAlgorithms.HmacSha256 - ), - Issuer = Configuration.PublicUrl, - Audience = Configuration.PublicUrl - }; + DisplayName = scheme.DisplayName ?? scheme.Name, + Identifier = scheme.Name + }) + .ToArray(); + } - var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); - var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor); + [HttpGet("{identifier:alpha}")] + public async Task StartScheme([FromRoute] string identifier) + { + var scheme = await SchemeProvider.GetSchemeAsync(identifier); - var jwt = jwtSecurityTokenHandler.WriteToken(securityToken); - - return new() + // The check for the whitelist ensures a user isn't starting an auth flow + // which isn't meant for users + if (scheme == null || !SchemeWhitelist.Contains(scheme.Name)) { - AccessToken = jwt - }; + await Results + .Problem( + "Invalid scheme identifier provided", + statusCode: 404 + ) + .ExecuteAsync(HttpContext); + + return; + } + + await HttpContext.ChallengeAsync( + scheme.Name, + new AuthenticationProperties() + { + RedirectUri = "/" + } + ); } [Authorize] [HttpGet("check")] - public async Task Check() + public Task Check() { - var userIdStr = User.FindFirstValue("userId")!; - var userId = int.Parse(userIdStr); - var user = await UserRepository.Get().FirstAsync(x => x.Id == userId); + var username = User.FindFirstValue(ClaimTypes.Name)!; + var id = User.FindFirstValue(ClaimTypes.NameIdentifier)!; + var email = User.FindFirstValue(ClaimTypes.Email)!; + var userId = User.FindFirstValue("UserId")!; + var permissions = User.FindFirstValue("Permissions")!; - return new() + var claims = new List() { - Email = user.Email, - Username = user.Username, - Permissions = user.Permissions + new(ClaimTypes.Name, username), + new(ClaimTypes.NameIdentifier, id), + new(ClaimTypes.Email, email), + new("UserId", userId), + new("Permissions", permissions) }; + + return Task.FromResult( + claims.ToArray() + ); + } + + [HttpGet("logout")] + public async Task Logout() + { + await HttpContext.SignOutAsync(); + await Results.Redirect("/").ExecuteAsync(HttpContext); } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/LocalAuth/LocalAuthController.cs b/Moonlight.ApiServer/Http/Controllers/LocalAuth/LocalAuthController.cs new file mode 100644 index 00000000..68b90a75 --- /dev/null +++ b/Moonlight.ApiServer/Http/Controllers/LocalAuth/LocalAuthController.cs @@ -0,0 +1,204 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MoonCore.Exceptions; +using MoonCore.Extended.Abstractions; +using MoonCore.Extended.Helpers; +using MoonCore.Helpers; +using Moonlight.ApiServer.Configuration; +using Moonlight.ApiServer.Database.Entities; +using Moonlight.ApiServer.Implementations.LocalAuth; + +namespace Moonlight.ApiServer.Http.Controllers.LocalAuth; + +[ApiController] +[Route("api/localAuth")] +public class LocalAuthController : Controller +{ + private readonly DatabaseRepository UserRepository; + private readonly IServiceProvider ServiceProvider; + private readonly IAuthenticationService AuthenticationService; + private readonly IOptionsMonitor Options; + private readonly ILogger Logger; + private readonly AppConfiguration Configuration; + + public LocalAuthController( + DatabaseRepository userRepository, + IServiceProvider serviceProvider, + IAuthenticationService authenticationService, + IOptionsMonitor options, + ILogger logger, + AppConfiguration configuration + ) + { + UserRepository = userRepository; + ServiceProvider = serviceProvider; + AuthenticationService = authenticationService; + Options = options; + Logger = logger; + Configuration = configuration; + } + + [HttpGet] + [HttpGet("login")] + public async Task Login() + { + var html = await ComponentHelper.RenderComponent(ServiceProvider); + + return Results.Content(html, "text/html"); + } + + [HttpGet("register")] + public async Task Register() + { + var html = await ComponentHelper.RenderComponent(ServiceProvider); + + return Results.Content(html, "text/html"); + } + + [HttpPost] + [HttpPost("login")] + public async Task Login([FromForm] string email, [FromForm] string password) + { + try + { + // Perform login + var user = await InternalLogin(email, password); + + // Login user + var options = Options.Get(LocalAuthConstants.AuthenticationScheme); + + await AuthenticationService.SignInAsync(HttpContext, options.SignInScheme, new ClaimsPrincipal( + new ClaimsIdentity( + [ + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.Username) + ], + LocalAuthConstants.AuthenticationScheme + ) + ), new AuthenticationProperties()); + + // Redirect back to wasm app + return Results.Redirect("/"); + } + catch (Exception e) + { + string errorMessage; + + if (e is HttpApiException apiException) + errorMessage = apiException.Title; + else + { + errorMessage = "An internal error occured"; + Logger.LogError(e, "An unhandled error occured while logging in user"); + } + + var html = await ComponentHelper.RenderComponent(ServiceProvider, + parameters => { parameters["ErrorMessage"] = errorMessage; }); + + return Results.Content(html, "text/html"); + } + } + + [HttpPost("register")] + public async Task Register([FromForm] string email, [FromForm] string password, [FromForm] string username) + { + try + { + // Perform register + var user = await InternalRegister(username, email, password); + + // Login user + var options = Options.Get(LocalAuthConstants.AuthenticationScheme); + + await AuthenticationService.SignInAsync(HttpContext, options.SignInScheme, new ClaimsPrincipal( + new ClaimsIdentity( + [ + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.Username) + ], + LocalAuthConstants.AuthenticationScheme + ) + ), new AuthenticationProperties()); + + // Redirect back to wasm app + return Results.Redirect("/"); + } + catch (Exception e) + { + string errorMessage; + + if (e is HttpApiException apiException) + errorMessage = apiException.Title; + else + { + errorMessage = "An internal error occured"; + Logger.LogError(e, "An unhandled error occured while logging in user"); + } + + var html = await ComponentHelper.RenderComponent(ServiceProvider, + parameters => { parameters["ErrorMessage"] = errorMessage; }); + + return Results.Content(html, "text/html"); + } + } + + private async Task InternalRegister(string username, string email, string password) + { + email = email.ToLower(); + username = username.ToLower(); + + if (await UserRepository.Get().AnyAsync(x => x.Username == username)) + throw new HttpApiException("A account with that username already exists", 400); + + if (await UserRepository.Get().AnyAsync(x => x.Email == email)) + throw new HttpApiException("A account with that email already exists", 400); + + string[] permissions = []; + + if (Configuration.Authentication.FirstUserAdmin) + { + var count = await UserRepository + .Get() + .CountAsync(); + + if (count == 0) + permissions = ["*"]; + } + + var user = new User() + { + Username = username, + Email = email, + Password = HashHelper.Hash(password), + Permissions = permissions + }; + + var finalUser = await UserRepository.Add(user); + + return finalUser; + } + + private async Task InternalLogin(string email, string password) + { + email = email.ToLower(); + + var user = await UserRepository + .Get() + .FirstOrDefaultAsync(x => x.Email == email); + + if (user == null) + throw new HttpApiException("Invalid combination of email and password", 400); + + if (!HashHelper.Verify(password, user.Password)) + throw new HttpApiException("Invalid combination of email and password", 400); + + return user; + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/OAuth2/Login.razor b/Moonlight.ApiServer/Http/Controllers/LocalAuth/Login.razor similarity index 86% rename from Moonlight.ApiServer/Http/Controllers/OAuth2/Login.razor rename to Moonlight.ApiServer/Http/Controllers/LocalAuth/Login.razor index 57c6c3f2..6b235da6 100644 --- a/Moonlight.ApiServer/Http/Controllers/OAuth2/Login.razor +++ b/Moonlight.ApiServer/Http/Controllers/LocalAuth/Login.razor @@ -42,8 +42,7 @@

No account? - Create an account + Create an account

@@ -55,8 +54,5 @@ @code { - [Parameter] public string ClientId { get; set; } - [Parameter] public string RedirectUri { get; set; } - [Parameter] public string ResponseType { get; set; } [Parameter] public string? ErrorMessage { get; set; } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor b/Moonlight.ApiServer/Http/Controllers/LocalAuth/Register.razor similarity index 87% rename from Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor rename to Moonlight.ApiServer/Http/Controllers/LocalAuth/Register.razor index 5aa5591d..aec21a39 100644 --- a/Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor +++ b/Moonlight.ApiServer/Http/Controllers/LocalAuth/Register.razor @@ -47,8 +47,7 @@

Already registered? - Login into your account + Login into your account

@@ -59,8 +58,5 @@ @code { - [Parameter] public string ClientId { get; set; } - [Parameter] public string RedirectUri { get; set; } - [Parameter] public string ResponseType { get; set; } [Parameter] public string? ErrorMessage { get; set; } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs b/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs deleted file mode 100644 index 70ab3a51..00000000 --- a/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs +++ /dev/null @@ -1,317 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; -using MoonCore.Exceptions; -using MoonCore.Extended.Abstractions; -using MoonCore.Extended.Helpers; -using MoonCore.Helpers; -using Moonlight.ApiServer.Configuration; -using Moonlight.ApiServer.Database.Entities; -using Moonlight.Shared.Http.Responses.OAuth2; - -namespace Moonlight.ApiServer.Http.Controllers.OAuth2; - -[ApiController] -[Route("oauth2")] -public partial class OAuth2Controller : Controller -{ - private readonly AppConfiguration Configuration; - private readonly DatabaseRepository UserRepository; - - private readonly string ExpectedRedirectUri; - - public OAuth2Controller(AppConfiguration configuration, DatabaseRepository userRepository) - { - Configuration = configuration; - UserRepository = userRepository; - - ExpectedRedirectUri = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationRedirect) - ? Configuration.PublicUrl - : Configuration.Authentication.OAuth2.AuthorizationRedirect; - } - - [AllowAnonymous] - [HttpGet("authorize")] - public async Task Authorize( - [FromQuery(Name = "client_id")] string clientId, - [FromQuery(Name = "redirect_uri")] string redirectUri, - [FromQuery(Name = "response_type")] string responseType, - [FromQuery(Name = "view")] string view = "login" - ) - { - if (!Configuration.Authentication.EnableLocalOAuth2) - throw new HttpApiException("Local OAuth2 has been disabled", 403); - - if (Configuration.Authentication.OAuth2.ClientId != clientId || - redirectUri != ExpectedRedirectUri || - responseType != "code") - { - throw new HttpApiException("Invalid oauth2 request", 400); - } - - string html; - - if (view == "register") - { - html = await ComponentHelper.RenderComponent(HttpContext.RequestServices, parameters => - { - parameters.Add("ClientId", clientId); - parameters.Add("RedirectUri", redirectUri); - parameters.Add("ResponseType", responseType); - }); - } - else - { - html = await ComponentHelper.RenderComponent(HttpContext.RequestServices, parameters => - { - parameters.Add("ClientId", clientId); - parameters.Add("RedirectUri", redirectUri); - parameters.Add("ResponseType", responseType); - }); - } - - await Results - .Text(html, "text/html", Encoding.UTF8) - .ExecuteAsync(HttpContext); - } - - [AllowAnonymous] - [HttpPost("authorize")] - public async Task AuthorizePost( - [FromQuery(Name = "client_id")] string clientId, - [FromQuery(Name = "redirect_uri")] string redirectUri, - [FromQuery(Name = "response_type")] string responseType, - [FromForm(Name = "email")] [EmailAddress(ErrorMessage = "You need to provide a valid email address")] string email, - [FromForm(Name = "password")] string password, - [FromForm(Name = "username")] string username = "", - [FromQuery(Name = "view")] string view = "login" - ) - { - if (!Configuration.Authentication.EnableLocalOAuth2) - throw new HttpApiException("Local OAuth2 has been disabled", 403); - - if (Configuration.Authentication.OAuth2.ClientId != clientId || - redirectUri != ExpectedRedirectUri || - responseType != "code") - { - throw new HttpApiException("Invalid oauth2 request", 400); - } - - if (view == "register" && string.IsNullOrEmpty(username)) - throw new HttpApiException("You need to provide a username", 400); - - string? errorMessage = null; - - try - { - if (view == "register") - { - var user = await Register(username, email, password); - var code = await GenerateCode(user); - - Response.Redirect($"{redirectUri}?code={code}"); - } - else - { - var user = await Login(email, password); - var code = await GenerateCode(user); - - Response.Redirect($"{redirectUri}?code={code}"); - } - } - catch (HttpApiException e) - { - errorMessage = e.Title; - - string html; - - if (view == "register") - { - html = await ComponentHelper.RenderComponent(HttpContext.RequestServices, parameters => - { - parameters.Add("ClientId", clientId); - parameters.Add("RedirectUri", redirectUri); - parameters.Add("ResponseType", responseType); - parameters.Add("ErrorMessage", errorMessage!); - }); - } - else - { - html = await ComponentHelper.RenderComponent(HttpContext.RequestServices, parameters => - { - parameters.Add("ClientId", clientId); - parameters.Add("RedirectUri", redirectUri); - parameters.Add("ResponseType", responseType); - parameters.Add("ErrorMessage", errorMessage!); - }); - } - - await Results - .Text(html, "text/html", Encoding.UTF8) - .ExecuteAsync(HttpContext); - } - } - - [AllowAnonymous] - [HttpPost("handle")] - public async Task Handle( - [FromForm(Name = "grant_type")] string grantType, - [FromForm(Name = "code")] string code, - [FromForm(Name = "redirect_uri")] string redirectUri, - [FromForm(Name = "client_id")] string clientId - ) - { - if (!Configuration.Authentication.EnableLocalOAuth2) - throw new HttpApiException("Local OAuth2 has been disabled", 403); - - // Check header - if (!Request.Headers.ContainsKey("Authorization")) - throw new HttpApiException("You are missing the Authorization header", 400); - - var authorizationHeaderValue = Request.Headers["Authorization"].FirstOrDefault() ?? ""; - - if (authorizationHeaderValue != $"Basic {Configuration.Authentication.OAuth2.ClientSecret}") - throw new HttpApiException("Invalid Authorization header value", 400); - - // Check form - if (grantType != "authorization_code") - throw new HttpApiException("Invalid grant type provided", 400); - - if (clientId != Configuration.Authentication.OAuth2.ClientId) - throw new HttpApiException("Invalid client id provided", 400); - - if (redirectUri != ExpectedRedirectUri) - throw new HttpApiException("Invalid redirect uri provided", 400); - - var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); - - ClaimsPrincipal? codeData; - - try - { - codeData = jwtSecurityTokenHandler.ValidateToken(code, new TokenValidationParameters() - { - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes( - Configuration.Authentication.OAuth2.Secret - )), - ValidateIssuerSigningKey = true, - ValidateLifetime = true, - ClockSkew = TimeSpan.Zero, - ValidateAudience = false, - ValidateIssuer = false - }, out _); - } - catch (SecurityTokenException) - { - throw new HttpApiException("Invalid code provided", 400); - } - - if (codeData == null) - throw new HttpApiException("Invalid code provided", 400); - - var userIdClaim = codeData.Claims.FirstOrDefault(x => x.Type == "id"); - - if (userIdClaim == null) - throw new HttpApiException("Malformed code provided", 400); - - if (!int.TryParse(userIdClaim.Value, out var userId)) - throw new HttpApiException("Malformed code provided", 400); - - var user = UserRepository - .Get() - .FirstOrDefault(x => x.Id == userId); - - if (user == null) - throw new HttpApiException("Malformed code provided", 400); - - return new() - { - UserId = user.Id - }; - } - - private Task GenerateCode(User user) - { - var securityTokenDescriptor = new SecurityTokenDescriptor() - { - Expires = DateTime.Now.AddMinutes(1), - IssuedAt = DateTime.Now, - NotBefore = DateTime.Now.AddMinutes(-1), - Claims = new Dictionary() - { - { - "id", - user.Id - } - }, - SigningCredentials = new SigningCredentials( - new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(Configuration.Authentication.OAuth2.Secret) - ), - SecurityAlgorithms.HmacSha256 - ) - }; - - var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); - var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor); - - return Task.FromResult( - jwtSecurityTokenHandler.WriteToken(securityToken) - ); - } - - private async Task Register(string username, string email, string password) - { - if (await UserRepository.Get().AnyAsync(x => x.Username == username)) - throw new HttpApiException("A account with that username already exists", 400); - - if (await UserRepository.Get().AnyAsync(x => x.Email == email)) - throw new HttpApiException("A account with that email already exists", 400); - - if (!UsernameRegex().IsMatch(username)) - throw new HttpApiException("The username is only allowed to be contained out of small characters and numbers", 400); - - var user = new User() - { - Username = username, - Email = email, - Password = HashHelper.Hash(password), - }; - - if (Configuration.Authentication.OAuth2.FirstUserAdmin) - { - var userCount = await UserRepository.Get().CountAsync(); - - if (userCount == 0) - user.Permissions = ["*"]; - - } - - return await UserRepository.Add(user); - } - - private async Task Login(string email, string password) - { - var user = await UserRepository - .Get() - .FirstOrDefaultAsync(x => x.Email == email); - - if (user == null) - throw new HttpApiException("Invalid combination of email and password", 400); - - if (!HashHelper.Verify(password, user.Password)) - throw new HttpApiException("Invalid combination of email and password", 400); - - return user; - } - - [GeneratedRegex("^[a-z][a-z0-9]*$")] - private static partial Regex UsernameRegex(); -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/Diagnose/CoreConfigDiagnoseProvider.cs b/Moonlight.ApiServer/Implementations/Diagnose/CoreConfigDiagnoseProvider.cs index 689148aa..a70c044a 100644 --- a/Moonlight.ApiServer/Implementations/Diagnose/CoreConfigDiagnoseProvider.cs +++ b/Moonlight.ApiServer/Implementations/Diagnose/CoreConfigDiagnoseProvider.cs @@ -34,15 +34,8 @@ public class CoreConfigDiagnoseProvider : IDiagnoseProvider } config.Database.Password = CheckForNullOrEmpty(config.Database.Password); - - config.Authentication.OAuth2.ClientSecret = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientSecret); - - config.Authentication.OAuth2.Secret = CheckForNullOrEmpty(config.Authentication.OAuth2.Secret); - config.Authentication.Secret = CheckForNullOrEmpty(config.Authentication.Secret); - config.Authentication.OAuth2.ClientId = CheckForNullOrEmpty(config.Authentication.OAuth2.ClientId); - await archive.AddText( "core/config.txt", JsonSerializer.Serialize( diff --git a/Moonlight.ApiServer/Implementations/LocalAuth/LocalAuthConstants.cs b/Moonlight.ApiServer/Implementations/LocalAuth/LocalAuthConstants.cs new file mode 100644 index 00000000..1d35cdc6 --- /dev/null +++ b/Moonlight.ApiServer/Implementations/LocalAuth/LocalAuthConstants.cs @@ -0,0 +1,6 @@ +namespace Moonlight.ApiServer.Implementations.LocalAuth; + +public static class LocalAuthConstants +{ + public const string AuthenticationScheme = "LocalAuth"; +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/LocalAuth/LocalAuthHandler.cs b/Moonlight.ApiServer/Implementations/LocalAuth/LocalAuthHandler.cs new file mode 100644 index 00000000..8109bac6 --- /dev/null +++ b/Moonlight.ApiServer/Implementations/LocalAuth/LocalAuthHandler.cs @@ -0,0 +1,32 @@ +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Moonlight.ApiServer.Implementations.LocalAuth; + +public class LocalAuthHandler : AuthenticationHandler +{ + public LocalAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder + ) : base(options, logger, encoder) + { + } + + protected override Task HandleAuthenticateAsync() + { + return Task.FromResult( + AuthenticateResult.Fail("Local authentication does not directly support AuthenticateAsync") + ); + } + + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + await Results + .Redirect("/api/localAuth") + .ExecuteAsync(Context); + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/LocalAuth/LocalAuthOptions.cs b/Moonlight.ApiServer/Implementations/LocalAuth/LocalAuthOptions.cs new file mode 100644 index 00000000..8292ef44 --- /dev/null +++ b/Moonlight.ApiServer/Implementations/LocalAuth/LocalAuthOptions.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Authentication; + +namespace Moonlight.ApiServer.Implementations.LocalAuth; + +public class LocalAuthOptions : AuthenticationSchemeOptions +{ + public string? SignInScheme { get; set; } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/LocalOAuth2Provider.cs b/Moonlight.ApiServer/Implementations/LocalOAuth2Provider.cs deleted file mode 100644 index e763ff65..00000000 --- a/Moonlight.ApiServer/Implementations/LocalOAuth2Provider.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using MoonCore.Exceptions; -using MoonCore.Extended.Abstractions; -using MoonCore.Helpers; -using Moonlight.ApiServer.Configuration; -using Moonlight.ApiServer.Database.Entities; -using Moonlight.ApiServer.Interfaces; -using Moonlight.Shared.Http.Responses.OAuth2; - -namespace Moonlight.ApiServer.Implementations; - -public class LocalOAuth2Provider : IOAuth2Provider -{ - private readonly AppConfiguration Configuration; - private readonly ILogger Logger; - private readonly DatabaseRepository UserRepository; - - public LocalOAuth2Provider( - AppConfiguration configuration, - ILogger logger, - DatabaseRepository userRepository - ) - { - UserRepository = userRepository; - Configuration = configuration; - Logger = logger; - } - - public Task Start() - { - var redirectUri = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationRedirect) - ? Configuration.PublicUrl - : Configuration.Authentication.OAuth2.AuthorizationRedirect; - - var endpoint = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationEndpoint) - ? Configuration.PublicUrl + "/oauth2/authorize" - : Configuration.Authentication.OAuth2.AuthorizationEndpoint; - - var clientId = Configuration.Authentication.OAuth2.ClientId; - - var url = $"{endpoint}" + - $"?client_id={clientId}" + - $"&redirect_uri={redirectUri}" + - $"&response_type=code"; - - return Task.FromResult(url); - } - - public async Task Complete(string code) - { - // Create http client to call the auth provider - var httpClient = new HttpClient(); - using var httpApiClient = new HttpApiClient(httpClient); - - httpClient.DefaultRequestHeaders.Add("Authorization", - $"Basic {Configuration.Authentication.OAuth2.ClientSecret}"); - - // Build access endpoint - var accessEndpoint = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AccessEndpoint) - ? $"{Configuration.PublicUrl}/oauth2/handle" - : Configuration.Authentication.OAuth2.AccessEndpoint; - - // Build redirect uri - var redirectUri = string.IsNullOrEmpty(Configuration.Authentication.OAuth2.AuthorizationRedirect) - ? Configuration.PublicUrl - : Configuration.Authentication.OAuth2.AuthorizationRedirect; - - // Call the auth provider - OAuth2HandleResponse handleData; - - try - { - handleData = await httpApiClient.PostJson(accessEndpoint, new FormUrlEncodedContent( - [ - new KeyValuePair("grant_type", "authorization_code"), - new KeyValuePair("code", code), - new KeyValuePair("redirect_uri", redirectUri), - new KeyValuePair("client_id", Configuration.Authentication.OAuth2.ClientId) - ] - )); - } - catch (HttpApiException e) - { - if (e.Status == 400) - Logger.LogTrace("The auth server returned an error: {e}", e); - else - Logger.LogCritical("The auth server returned an error: {e}", e); - - throw new HttpApiException("Unable to request user data", 500); - } - - // Notice: We just look up the user id here - // which works as our oauth2 provider is using the same db. - // a real oauth2 provider would create a user here - - // Handle the returned data - var userId = handleData.UserId; - - var user = await UserRepository - .Get() - .FirstOrDefaultAsync(x => x.Id == userId); - - return user; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/UserAuthInvalidation.cs b/Moonlight.ApiServer/Implementations/UserAuthInvalidation.cs deleted file mode 100644 index 000dbb32..00000000 --- a/Moonlight.ApiServer/Implementations/UserAuthInvalidation.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Security.Claims; -using Microsoft.EntityFrameworkCore; -using MoonCore.Extended.Abstractions; -using MoonCore.Extended.JwtInvalidation; -using Moonlight.ApiServer.Database.Entities; - -namespace Moonlight.ApiServer.Implementations; - -public class UserAuthInvalidation : IJwtInvalidateHandler -{ - private readonly DatabaseRepository UserRepository; - private readonly DatabaseRepository ApiKeyRepository; - - public UserAuthInvalidation( - DatabaseRepository userRepository, - DatabaseRepository apiKeyRepository - ) - { - UserRepository = userRepository; - ApiKeyRepository = apiKeyRepository; - } - - public async Task Handle(ClaimsPrincipal principal) - { - var userIdClaim = principal.FindFirstValue("userId"); - - if (!string.IsNullOrEmpty(userIdClaim)) - { - var userId = int.Parse(userIdClaim); - - var user = await UserRepository - .Get() - .FirstOrDefaultAsync(x => x.Id == userId); - - if (user == null) - return true; // User is deleted, invalidate session - - var iatStr = principal.FindFirstValue("iat")!; - var iat = DateTimeOffset.FromUnixTimeSeconds(long.Parse(iatStr)); - - // If the token has been issued before the token valid time, its expired, and we want to invalidate it - return user.TokenValidTimestamp > iat; - } - - var apiKeyIdClaim = principal.FindFirstValue("apiKeyId"); - - if (!string.IsNullOrEmpty(apiKeyIdClaim)) - { - var apiKeyId = int.Parse(apiKeyIdClaim); - - var apiKey = await ApiKeyRepository - .Get() - .FirstOrDefaultAsync(x => x.Id == apiKeyId); - - // If the api key exists, we don't want to invalidate the request. - // If it doesn't exist we want to invalidate the request - return apiKey == null; - } - - return true; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Interfaces/IOAuth2Provider.cs b/Moonlight.ApiServer/Interfaces/IOAuth2Provider.cs deleted file mode 100644 index 3d9d19c5..00000000 --- a/Moonlight.ApiServer/Interfaces/IOAuth2Provider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Moonlight.ApiServer.Database.Entities; - -namespace Moonlight.ApiServer.Interfaces; - -public interface IOAuth2Provider -{ - public Task Start(); - - public Task Complete(string code); -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index e5fee3df..76796e54 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -1,47 +1,47 @@  - - net9.0 - enable - enable - - - - - - - - - - Moonlight.ApiServer - 2.1.7 - Moonlight Panel - A build of the api server for moonlight development - https://github.com/Moonlight-Panel/Moonlight - true - apiserver - true - - - - - - - - - - - - - - - - - - - - - - - + + net9.0 + enable + enable + + + + + + + + + + Moonlight.ApiServer + 2.1.7 + Moonlight Panel + A build of the api server for moonlight development + https://github.com/Moonlight-Panel/Moonlight + true + apiserver + true + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/ApiKeyAuthService.cs b/Moonlight.ApiServer/Services/ApiKeyAuthService.cs new file mode 100644 index 00000000..15589fe8 --- /dev/null +++ b/Moonlight.ApiServer/Services/ApiKeyAuthService.cs @@ -0,0 +1,32 @@ +using System.Security.Claims; +using Microsoft.EntityFrameworkCore; +using MoonCore.Extended.Abstractions; +using Moonlight.ApiServer.Database.Entities; + +namespace Moonlight.ApiServer.Services; + +public class ApiKeyAuthService +{ + private readonly DatabaseRepository ApiKeyRepository; + + public ApiKeyAuthService(DatabaseRepository apiKeyRepository) + { + ApiKeyRepository = apiKeyRepository; + } + + public async Task Validate(ClaimsPrincipal? principal) + { + // Ignore malformed claims principal + if (principal is not { Identity.IsAuthenticated: true }) + return false; + + var apiKeyIdStr = principal.FindFirstValue("ApiKeyId"); + + if (!int.TryParse(apiKeyIdStr, out var apiKeyId)) + return false; + + return await ApiKeyRepository + .Get() + .AnyAsync(x => x.Id == apiKeyId); + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/ApiKeyService.cs b/Moonlight.ApiServer/Services/ApiKeyService.cs index 6910ae71..177bd013 100644 --- a/Moonlight.ApiServer/Services/ApiKeyService.cs +++ b/Moonlight.ApiServer/Services/ApiKeyService.cs @@ -29,11 +29,11 @@ public class ApiKeyService Claims = new Dictionary() { { - "apiKeyId", + "ApiKeyId", apiKey.Id }, { - "permissions", + "Permissions", string.Join(";", apiKey.Permissions) } }, diff --git a/Moonlight.ApiServer/Services/UserAuthService.cs b/Moonlight.ApiServer/Services/UserAuthService.cs new file mode 100644 index 00000000..8b54c1d1 --- /dev/null +++ b/Moonlight.ApiServer/Services/UserAuthService.cs @@ -0,0 +1,142 @@ +using System.Security.Claims; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using MoonCore.Extended.Abstractions; +using MoonCore.Extended.Helpers; +using MoonCore.Helpers; +using Moonlight.ApiServer.Configuration; +using Moonlight.ApiServer.Database.Entities; + +namespace Moonlight.ApiServer.Services; + +public class UserAuthService +{ + private readonly ILogger Logger; + private readonly DatabaseRepository UserRepository; + private readonly AppConfiguration Configuration; + + private const string UserIdClaim = "UserId"; + private const string IssuedAtClaim = "IssuedAt"; + + public UserAuthService( + ILogger logger, + DatabaseRepository userRepository, + AppConfiguration configuration + ) + { + Logger = logger; + UserRepository = userRepository; + Configuration = configuration; + } + + public async Task Sync(ClaimsPrincipal? principal) + { + // Ignore malformed claims principal + if (principal is not { Identity.IsAuthenticated: true }) + return false; + + // Search for email and username. We need both to create the user model if required. + // We do a ToLower here because external authentication provider might provide case-sensitive data + + var email = principal.FindFirstValue(ClaimTypes.Email)?.ToLower(); + var username = principal.FindFirstValue(ClaimTypes.Name)?.ToLower(); + + if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(username)) + { + Logger.LogWarning( + "The authentication scheme {scheme} did not provide claim types: email, name. These are required to sync to user to the database", + principal.Identity.AuthenticationType + ); + + return false; + } + + // If you plan to use multiple auth providers it can be a good idea + // to use an identifier in the user model which consists of the provider and the NameIdentifier + // instead of the email address. For simplicity, we just use the email as the identifier so multiple auth providers + // can lead to the same account when the email matches + var user = await UserRepository + .Get() + .FirstOrDefaultAsync(u => u.Email == email); + + if (user == null) + { + string[] permissions = []; + + // Yes I know we handle the first user admin thing in the LocalAuth too, + // but this only works fo the local auth. So if a user uses an external auth scheme + // like oauth2 discord, the first user admin toggle would do nothing + if (Configuration.Authentication.FirstUserAdmin) + { + var count = await UserRepository + .Get() + .CountAsync(); + + if (count == 0) + permissions = ["*"]; + } + + user = await UserRepository.Add(new User() + { + Email = email, + TokenValidTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1), + Username = username, + Password = HashHelper.Hash(Formatter.GenerateString(64)), + Permissions = permissions + }); + } + + // You can sync other properties here + if (user.Username != username) + { + user.Username = username; + await UserRepository.Update(user); + } + + principal.Identities.First().AddClaims([ + new Claim(UserIdClaim, user.Id.ToString()), + new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), + new Claim("Permissions", string.Join(';', user.Permissions)) + ]); + + return true; + } + + public async Task Validate(ClaimsPrincipal? principal) + { + // Ignore malformed claims principal + if (principal is not { Identity.IsAuthenticated: true }) + return false; + + // Validate if the user still exists, and then we want to validate the token issue time + // against the invalidation time + + var userIdStr = principal.FindFirstValue(UserIdClaim); + + if (!int.TryParse(userIdStr, out var userId)) + return false; + + var user = await UserRepository + .Get() + .FirstOrDefaultAsync(u => u.Id == userId); + + if (user == null) + return false; + + // Token time validation + var issuedAtStr = principal.FindFirstValue(IssuedAtClaim); + + if (!long.TryParse(issuedAtStr, out var issuedAtUnix)) + return false; + + var issuedAt = DateTimeOffset + .FromUnixTimeSeconds(issuedAtUnix) + .ToUniversalTime(); + + // If the issued at timestamp is greater than the token validation timestamp + // everything is fine. If not it means that the token should be invalidated + // as it is too old + + return issuedAt > user.TokenValidTimestamp; + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/UserService.cs b/Moonlight.ApiServer/Services/UserDeletionService.cs similarity index 100% rename from Moonlight.ApiServer/Services/UserService.cs rename to Moonlight.ApiServer/Services/UserDeletionService.cs diff --git a/Moonlight.ApiServer/Startup/Startup.Auth.cs b/Moonlight.ApiServer/Startup/Startup.Auth.cs index 04abceb7..0a0995e1 100644 --- a/Moonlight.ApiServer/Startup/Startup.Auth.cs +++ b/Moonlight.ApiServer/Startup/Startup.Auth.cs @@ -1,11 +1,12 @@ using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; -using MoonCore.Extended.JwtInvalidation; using MoonCore.Permissions; -using Moonlight.ApiServer.Implementations; -using Moonlight.ApiServer.Interfaces; +using Moonlight.ApiServer.Implementations.LocalAuth; using Moonlight.ApiServer.Services; namespace Moonlight.ApiServer.Startup; @@ -15,8 +16,25 @@ public partial class Startup private Task RegisterAuth() { WebApplicationBuilder.Services - .AddAuthentication("coreAuthentication") - .AddJwtBearer("coreAuthentication", options => + .AddAuthentication(options => { options.DefaultScheme = "MainScheme"; }) + .AddPolicyScheme("MainScheme", null, options => + { + // If an api key is specified via the bearer auth header + // we want to use the ApiKey scheme for authenticating the request + options.ForwardDefaultSelector = context => + { + if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader)) + return "Session"; + + var auth = authHeader.FirstOrDefault(); + + if (string.IsNullOrEmpty(auth) || !auth.StartsWith("Bearer ")) + return "Session"; + + return "ApiKey"; + }; + }) + .AddJwtBearer("ApiKey", null, options => { options.TokenValidationParameters = new() { @@ -31,22 +49,116 @@ public partial class Startup ValidateIssuer = true, ValidIssuer = Configuration.PublicUrl }; + + options.Events = new JwtBearerEvents() + { + OnTokenValidated = async context => + { + var apiKeyAuthService = context + .HttpContext + .RequestServices + .GetRequiredService(); + + var result = await apiKeyAuthService.Validate(context.Principal); + + if (!result) + context.Fail("API key has been deleted"); + } + }; + }) + .AddCookie("Session", null, options => + { + options.ExpireTimeSpan = TimeSpan.FromDays(Configuration.Authentication.Sessions.ExpiresIn); + + options.Cookie = new CookieBuilder() + { + Name = Configuration.Authentication.Sessions.CookieName, + Path = "/", + IsEssential = true, + SecurePolicy = CookieSecurePolicy.SameAsRequest + }; + + // As redirects won't work in our spa which uses API calls + // we need to customize the responses when certain actions happen + options.Events.OnRedirectToLogin = async context => + { + await Results.Problem( + title: "Unauthenticated", + detail: "You need to authenticate yourself to use this endpoint", + statusCode: 401 + ) + .ExecuteAsync(context.HttpContext); + }; + + options.Events.OnRedirectToAccessDenied = async context => + { + await Results.Problem( + title: "Permission denied", + detail: "You are missing the required permissions to access this endpoint", + statusCode: 403 + ) + .ExecuteAsync(context.HttpContext); + }; + + options.Events.OnSigningIn = async context => + { + var userSyncService = context + .HttpContext + .RequestServices + .GetRequiredService(); + + var result = await userSyncService.Sync(context.Principal); + + if (!result) + context.Principal = new(); + else + context.Properties.IsPersistent = true; + }; + + options.Events.OnValidatePrincipal = async context => + { + var userSyncService = context + .HttpContext + .RequestServices + .GetRequiredService(); + + var result = await userSyncService.Validate(context.Principal); + + if (!result) + context.RejectPrincipal(); + }; + }) + .AddScheme(LocalAuthConstants.AuthenticationScheme, "Local Auth", options => + { + options.ForwardAuthenticate = "Session"; + options.ForwardSignIn = "Session"; + options.ForwardSignOut = "Session"; + + options.SignInScheme = "Session"; }); - - WebApplicationBuilder.Services.AddJwtBearerInvalidation("coreAuthentication"); - WebApplicationBuilder.Services.AddScoped(); WebApplicationBuilder.Services.AddAuthorization(); WebApplicationBuilder.Services.AddAuthorizationPermissions(options => { - options.ClaimName = "permissions"; + options.ClaimName = "Permissions"; options.Prefix = "permissions:"; }); + + WebApplicationBuilder.Services.AddScoped(); + WebApplicationBuilder.Services.AddScoped(); - // Add local oauth2 provider if enabled - if (Configuration.Authentication.EnableLocalOAuth2) - WebApplicationBuilder.Services.AddScoped(); + // Setup data protection storage within storage folder + // so its persists in containers + var dpKeyPath = Path.Combine("storage", "dataProtectionKeys"); + + Directory.CreateDirectory(dpKeyPath); + + WebApplicationBuilder.Services + .AddDataProtection() + .PersistKeysToFileSystem( + new DirectoryInfo(dpKeyPath) + ); WebApplicationBuilder.Services.AddScoped(); diff --git a/Moonlight.ApiServer/Startup/Startup.Logging.cs b/Moonlight.ApiServer/Startup/Startup.Logging.cs index 49411c03..2bc18b69 100644 --- a/Moonlight.ApiServer/Startup/Startup.Logging.cs +++ b/Moonlight.ApiServer/Startup/Startup.Logging.cs @@ -33,7 +33,8 @@ public partial class Startup { { "Default", "Information" }, { "Microsoft.AspNetCore", "Warning" }, - { "System.Net.Http.HttpClient", "Warning" } + { "System.Net.Http.HttpClient", "Warning" }, + { "Moonlight.ApiServer.Implementations.LocalAuth.LocalAuthHandler", "Warning" } }; var logLevelsJson = JsonSerializer.Serialize(defaultLogLevels); diff --git a/Moonlight.Client/Implementations/LogErrorFilter.cs b/Moonlight.Client/Implementations/LogErrorFilter.cs new file mode 100644 index 00000000..5ce50bef --- /dev/null +++ b/Moonlight.Client/Implementations/LogErrorFilter.cs @@ -0,0 +1,19 @@ +using MoonCore.Blazor.FlyonUi.Exceptions; + +namespace Moonlight.Client.Implementations; + +public class LogErrorFilter : IGlobalErrorFilter +{ + private readonly ILogger Logger; + + public LogErrorFilter(ILogger logger) + { + Logger = logger; + } + + public Task HandleException(Exception ex) + { + Logger.LogError(ex, "Global error processed"); + return Task.FromResult(false); + } +} \ No newline at end of file diff --git a/Moonlight.Client/Implementations/UnauthenticatedErrorFilter.cs b/Moonlight.Client/Implementations/UnauthenticatedErrorFilter.cs new file mode 100644 index 00000000..1824f86f --- /dev/null +++ b/Moonlight.Client/Implementations/UnauthenticatedErrorFilter.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Components; +using MoonCore.Blazor.FlyonUi.Exceptions; +using MoonCore.Exceptions; + +namespace Moonlight.Client.Implementations; + +public class UnauthenticatedErrorFilter : IGlobalErrorFilter +{ + private readonly NavigationManager Navigation; + + public UnauthenticatedErrorFilter(NavigationManager navigation) + { + Navigation = navigation; + } + + public Task HandleException(Exception ex) + { + if (ex is not HttpApiException { Status: 401 }) + return Task.FromResult(false); + + Navigation.NavigateTo("/api/auth/logout", true); + return Task.FromResult(true); + } +} \ No newline at end of file diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index bb62fbee..bf1c15fc 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -22,9 +22,11 @@ - + + - + + diff --git a/Moonlight.Client/Services/RemoteAuthStateManager.cs b/Moonlight.Client/Services/RemoteAuthStateManager.cs deleted file mode 100644 index 6af53153..00000000 --- a/Moonlight.Client/Services/RemoteAuthStateManager.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Security.Claims; -using System.Web; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Authorization; -using MoonCore.Blazor.FlyonUi.Auth; -using MoonCore.Blazor.Services; -using MoonCore.Exceptions; -using MoonCore.Helpers; -using Moonlight.Shared.Http.Requests.Auth; -using Moonlight.Shared.Http.Responses.Auth; - -namespace Moonlight.Client.Services; - -public class RemoteAuthStateManager : AuthenticationStateManager -{ - private readonly NavigationManager NavigationManager; - private readonly HttpApiClient HttpApiClient; - private readonly LocalStorageService LocalStorageService; - private readonly ILogger Logger; - - public RemoteAuthStateManager( - HttpApiClient httpApiClient, - LocalStorageService localStorageService, - NavigationManager navigationManager, - ILogger logger - ) - { - HttpApiClient = httpApiClient; - LocalStorageService = localStorageService; - NavigationManager = navigationManager; - Logger = logger; - } - - public override async Task GetAuthenticationStateAsync() - => await LoadAuthState(); - - public override async Task HandleLogin() - { - var uri = new Uri(NavigationManager.Uri); - var codeParam = HttpUtility.ParseQueryString(uri.Query).Get("code"); - - if (string.IsNullOrEmpty(codeParam)) // If this is true, we need to log in the user - { - await StartLogin(); - } - else - { - try - { - var loginCompleteData = await HttpApiClient.PostJson( - "api/auth/complete", - new LoginCompleteRequest() - { - Code = codeParam - } - ); - - await LocalStorageService.SetString("AccessToken", loginCompleteData.AccessToken); - - NavigationManager.NavigateTo("/"); - NotifyAuthenticationStateChanged(LoadAuthState()); - } - catch (HttpApiException e) - { - Logger.LogError("Unable to complete login: {e}", e); - - await StartLogin(); - } - } - } - - public override async Task Logout() - { - if (await LocalStorageService.ContainsKey("AccessToken")) - await LocalStorageService.SetString("AccessToken", ""); - - NotifyAuthenticationStateChanged(LoadAuthState()); - } - - #region Utilities - - private async Task StartLogin() - { - var loginStartData = await HttpApiClient.GetJson("api/auth/start"); - - NavigationManager.NavigateTo(loginStartData.Url, true); - } - - private async Task LoadAuthState() - { - AuthenticationState newState; - - try - { - var checkData = await HttpApiClient.GetJson("api/auth/check"); - - newState = new(new ClaimsPrincipal( - new ClaimsIdentity( - [ - new Claim("username", checkData.Username), - new Claim("email", checkData.Email), - new Claim("permissions", string.Join(";", checkData.Permissions)) - ], - "RemoteAuthStateManager" - ) - )); - } - catch (HttpApiException) - { - newState = new(new ClaimsPrincipal( - new ClaimsIdentity() - )); - } - - return newState; - } - - #endregion -} \ No newline at end of file diff --git a/Moonlight.Client/Services/RemoteAuthStateProvider.cs b/Moonlight.Client/Services/RemoteAuthStateProvider.cs new file mode 100644 index 00000000..74635da7 --- /dev/null +++ b/Moonlight.Client/Services/RemoteAuthStateProvider.cs @@ -0,0 +1,45 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using MoonCore.Exceptions; +using MoonCore.Helpers; +using Moonlight.Shared.Http.Responses.Auth; + +namespace Moonlight.Client.Services; + +public class RemoteAuthStateProvider : AuthenticationStateProvider +{ + private readonly HttpApiClient ApiClient; + + public RemoteAuthStateProvider(HttpApiClient apiClient) + { + ApiClient = apiClient; + } + + public override async Task GetAuthenticationStateAsync() + { + ClaimsPrincipal principal; + + try + { + var claims = await ApiClient.GetJson( + "api/auth/check" + ); + + principal = new ClaimsPrincipal( + new ClaimsIdentity( + claims.Select(x => new Claim(x.Type, x.Value)), + "RemoteAuthentication" + ) + ); + } + catch (HttpApiException e) + { + if (e.Status != 401 && e.Status != 403) + throw; + + principal = new ClaimsPrincipal(); + } + + return new AuthenticationState(principal); + } +} \ No newline at end of file diff --git a/Moonlight.Client/Startup/Startup.Auth.cs b/Moonlight.Client/Startup/Startup.Auth.cs index f5677ea1..6f95b4ba 100644 --- a/Moonlight.Client/Startup/Startup.Auth.cs +++ b/Moonlight.Client/Startup/Startup.Auth.cs @@ -1,6 +1,8 @@ +using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; -using MoonCore.Blazor.FlyonUi.Auth; +using MoonCore.Blazor.FlyonUi.Exceptions; using MoonCore.Permissions; +using Moonlight.Client.Implementations; using Moonlight.Client.Services; namespace Moonlight.Client.Startup; @@ -12,11 +14,12 @@ public partial class Startup WebAssemblyHostBuilder.Services.AddAuthorizationCore(); WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState(); - WebAssemblyHostBuilder.Services.AddAuthenticationStateManager(); + WebAssemblyHostBuilder.Services.AddScoped(); + WebAssemblyHostBuilder.Services.AddScoped(); WebAssemblyHostBuilder.Services.AddAuthorizationPermissions(options => { - options.ClaimName = "permissions"; + options.ClaimName = "Permissions"; options.Prefix = "permissions:"; }); diff --git a/Moonlight.Client/Startup/Startup.Base.cs b/Moonlight.Client/Startup/Startup.Base.cs index 447c64b4..407ac44b 100644 --- a/Moonlight.Client/Startup/Startup.Base.cs +++ b/Moonlight.Client/Startup/Startup.Base.cs @@ -25,27 +25,11 @@ public partial class Startup WebAssemblyHostBuilder.Services.AddScoped(sp => { var httpClient = sp.GetRequiredService(); - var httpApiClient = new HttpApiClient(httpClient); - - var localStorageService = sp.GetRequiredService(); - - httpApiClient.OnConfigureRequest += async request => - { - var accessToken = await localStorageService.GetString("AccessToken"); - - if (string.IsNullOrEmpty(accessToken)) - return; - - request.Headers.Add("Authorization", $"Bearer {accessToken}"); - }; - - return httpApiClient; + return new HttpApiClient(httpClient); }); - WebAssemblyHostBuilder.Services.AddScoped(); WebAssemblyHostBuilder.Services.AddFileManagerOperations(); WebAssemblyHostBuilder.Services.AddFlyonUiServices(); - WebAssemblyHostBuilder.Services.AddScoped(); WebAssemblyHostBuilder.Services.AddScoped(); diff --git a/Moonlight.Client/Startup/Startup.Logging.cs b/Moonlight.Client/Startup/Startup.Logging.cs index 672b0ed5..1b2a2714 100644 --- a/Moonlight.Client/Startup/Startup.Logging.cs +++ b/Moonlight.Client/Startup/Startup.Logging.cs @@ -1,4 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; +using MoonCore.Blazor.FlyonUi.Exceptions; using MoonCore.Logging; +using Moonlight.Client.Implementations; namespace Moonlight.Client.Startup; @@ -7,6 +10,7 @@ public partial class Startup private Task SetupLogging() { var loggerFactory = new LoggerFactory(); + loggerFactory.AddAnsiConsole(); Logger = loggerFactory.CreateLogger(); @@ -19,6 +23,8 @@ public partial class Startup WebAssemblyHostBuilder.Logging.ClearProviders(); WebAssemblyHostBuilder.Logging.AddAnsiConsole(); + WebAssemblyHostBuilder.Services.AddScoped(); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/Moonlight.Client/Startup/Startup.cs b/Moonlight.Client/Startup/Startup.cs index 1d568b6b..08e0bab5 100644 --- a/Moonlight.Client/Startup/Startup.cs +++ b/Moonlight.Client/Startup/Startup.cs @@ -28,8 +28,8 @@ public partial class Startup WebAssemblyHostBuilder = builder; await PrintVersion(); - await SetupLogging(); + await SetupLogging(); await LoadConfiguration(); await InitializePlugins(); diff --git a/Moonlight.Client/Styles/MoonCore.Blazor.FlyonUi/mooncore.map b/Moonlight.Client/Styles/MoonCore.Blazor.FlyonUi/mooncore.map new file mode 100644 index 00000000..31c2b5c9 --- /dev/null +++ b/Moonlight.Client/Styles/MoonCore.Blazor.FlyonUi/mooncore.map @@ -0,0 +1,534 @@ +!bg-base-100 +!border-base-content/40 +!border-none +!flex +!font-medium +!font-semibold +!h-2.5 +!justify-between +!me-1.5 +!ms-auto +!px-2.5 +!py-0.5 +!rounded-full +!rounded-xs +!text-sm +!w-2.5 +*:[grid-area:1/1] +*:first:rounded-tl-lg +*:last:rounded-tr-lg +-left-4 +-ml-4 +-translate-x-full +-translate-y-1/2 +absolute +accordion +accordion-bordered +accordion-content +accordion-toggle +active +active-tab:bg-primary +active-tab:hover:text-primary-content +active-tab:text-primary-content +advance-select-menu +advance-select-option +advance-select-tag +advance-select-toggle +alert +alert-error +alert-outline +alert-soft +align-bottom +align-middle +animate-bounce +animate-ping +aria-[current='page']:text-bg-soft-primary +avatar +badge +badge-error +badge-info +badge-outline +badge-primary +badge-soft +badge-success +bg-background +bg-background/60 +bg-base-100 +bg-base-150 +bg-base-200 +bg-base-200! +bg-base-200/50 +bg-base-300 +bg-base-300/45 +bg-base-300/50 +bg-base-300/60 +bg-error +bg-info +bg-primary +bg-primary/5 +bg-success +bg-transparent +bg-warning +block +blur +border +border-0 +border-2 +border-b +border-base-content +border-base-content/20 +border-base-content/25 +border-base-content/40 +border-base-content/5 +border-dashed +border-t +border-transparent +bottom-0 +bottom-full +break-words +btn +btn-accent +btn-active +btn-circle +btn-disabled +btn-error +btn-info +btn-outline +btn-primary +btn-secondary +btn-sm +btn-soft +btn-square +btn-success +btn-text +btn-warning +card +card-alert +card-body +card-border +card-footer +card-header +card-title +carousel +carousel-body +carousel-next +carousel-prev +carousel-slide +chat +chat-avatar +chat-bubble +chat-footer +chat-header +chat-receiver +chat-sender +checkbox +checkbox-primary +checkbox-xs +col-span-1 +collapse +combo-box-selected:block +combo-box-selected:dropdown-active +complete +container +contents +cursor-default +cursor-not-allowed +cursor-pointer +diff +disabled +divide-base-150/60 +divide-y +divider +drop-shadow +dropdown +dropdown-active +dropdown-disabled +dropdown-item +dropdown-menu +dropdown-open:opacity-100 +dropdown-open:rotate-180 +dropdown-toggle +duration-300 +duration-500 +ease-in-out +ease-linear +end-3 +file-upload-complete:progress-success +fill-base-content +fill-black +fill-gray-200 +filter +filter-reset +fixed +flex +flex-1 +flex-col +flex-grow +flex-nowrap +flex-row +flex-shrink-0 +flex-wrap +focus-visible:outline-none +focus-within:border-primary +focus:border-primary +focus:outline-1 +focus:outline-none +focus:outline-primary +focus:ring-0 +font-bold +font-inter +font-medium +font-normal +font-semibold +gap-0.5 +gap-1 +gap-1.5 +gap-2 +gap-3 +gap-4 +gap-5 +gap-6 +gap-x-1 +gap-x-2 +gap-x-3 +gap-y-1 +gap-y-2.5 +gap-y-3 +grid +grid-cols-1 +grid-cols-4 +grid-flow-col +grow +grow-0 +h-12 +h-2 +h-3 +h-32 +h-64 +h-8 +h-auto +h-full +h-screen +helper-text +hidden +hover:bg-primary/5 +hover:bg-transparent +hover:text-base-content +hover:text-base-content/60 +hover:text-primary +image-full +inline +inline-block +inline-flex +inline-grid +input +input-floating +input-floating-label +input-lg +input-md +input-sm +input-xl +inset-0 +inset-y-0 +inset-y-2 +invisible +is-invalid +is-valid +isolate +italic +items-center +items-end +items-start +join +join-item +justify-between +justify-center +justify-end +justify-start +justify-stretch +label-text +leading-3 +leading-3.5 +leading-6 +leading-none +left-0 +lg:bg-base-100/20 +lg:flex +lg:gap-y-0 +lg:grid-cols-2 +lg:hidden +lg:justify-end +lg:justify-start +lg:min-w-0 +lg:p-10 +lg:pb-5 +lg:pl-64 +lg:pr-3.5 +lg:pt-5 +lg:ring-1 +lg:ring-base-content/10 +lg:rounded-lg +lg:shadow-xs +link +link-animated +link-hover +list-disc +list-inside +list-none +loading +loading-lg +loading-sm +loading-spinner +loading-xl +loading-xs +lowercase +m-10 +mask +max-h-52 +max-lg:flex-col +max-lg:hidden +max-w-7xl +max-w-80 +max-w-full +max-w-lg +max-w-sm +max-w-xl +mb-0.5 +mb-1 +mb-1.5 +mb-2 +mb-2.5 +mb-3 +mb-4 +mb-5 +md:min-w-md +md:table-cell +md:text-3xl +me-1 +me-1.5 +me-2 +me-2.5 +me-5 +menu +menu-active +menu-disabled +menu-dropdown +menu-dropdown-show +menu-horizontal +menu-title +min-h-0 +min-h-svh +min-w-0 +min-w-28 +min-w-48 +min-w-60 +min-w-[100px] +min-w-sm +ml-3 +ml-4 +modal +modal-content +modal-dialog +modal-middle +modal-title +mr-4 +ms-0.5 +ms-1 +ms-2 +ms-3 +ms-auto +mt-1 +mt-1.5 +mt-10 +mt-12 +mt-2 +mt-2.5 +mt-3 +mt-3.5 +mt-4 +mt-5 +mt-8 +mx-1 +mx-auto +my-3 +my-auto +object-cover +opacity-0 +opacity-100 +open +origin-top-left +outline +outline-0 +overflow-hidden +overflow-x-auto +overflow-y-auto +overlay-open:duration-50 +overlay-open:opacity-100 +p-0.5 +p-1 +p-2 +p-3 +p-4 +p-5 +p-6 +p-8 +pin-input +placeholder-base-content/60 +pointer-events-auto +pointer-events-none +progress +progress-bar +progress-indeterminate +progress-primary +pt-0 +pt-0.5 +pt-3 +px-1.5 +px-2 +px-2.5 +px-3 +px-4 +px-5 +py-0.5 +py-1.5 +py-2 +py-2.5 +py-6 +radial-progress +radio +range +relative +resize +ring-0 +ring-1 +ring-white/10 +rounded-box +rounded-field +rounded-full +rounded-lg +rounded-md +rounded-t-lg +row-active +row-hover +rtl:!mr-0 +select +select-disabled:opacity-40 +select-disabled:pointer-events-none +select-floating +select-floating-label +selected +selected:select-active +shadow-base-300/20 +shadow-lg +shadow-md +shadow-xs +shrink-0 +size-10 +size-4 +size-5 +size-8 +skeleton +skeleton-animated +sm:auto-cols-max +sm:flex +sm:items-center +sm:items-end +sm:justify-between +sm:justify-end +sm:max-w-2xl +sm:max-w-3xl +sm:max-w-4xl +sm:max-w-5xl +sm:max-w-6xl +sm:max-w-7xl +sm:max-w-lg +sm:max-w-md +sm:max-w-xl +sm:mb-0 +sm:mt-5 +sm:mt-6 +sm:p-6 +sm:py-2 +sm:text-sm/5 +space-x-1 +space-y-1 +space-y-4 +sr-only +static +status +status-error +sticky +switch +tab +tab-active +table +table-pin-cols +table-pin-rows +tabs +tabs-bordered +tabs-lg +tabs-lifted +tabs-md +tabs-sm +tabs-xl +tabs-xs +text-2xl +text-4xl +text-accent +text-base +text-base-content +text-base-content/40 +text-base-content/50 +text-base-content/60 +text-base-content/70 +text-base-content/80 +text-base/6 +text-center +text-error +text-error-content +text-gray-400 +text-info +text-info-content +text-left +text-lg +text-primary +text-primary-content +text-sm +text-sm/5 +text-success +text-success-content +text-warning +text-warning-content +text-xl +text-xs +text-xs/5 +textarea +textarea-floating +textarea-floating-label +theme-controller +tooltip +tooltip-content +top-0 +top-1/2 +top-full +transform +transition +transition-all +transition-opacity +translate-x-0 +truncate +underline +uppercase +validate +w-0 +w-0.5 +w-12 +w-4 +w-56 +w-64 +w-fit +w-full +whitespace-nowrap +z-10 +z-40 +z-50 \ No newline at end of file diff --git a/Moonlight.Client/UI/App.razor b/Moonlight.Client/UI/App.razor index 056f1edc..6a5e06bf 100644 --- a/Moonlight.Client/UI/App.razor +++ b/Moonlight.Client/UI/App.razor @@ -1,8 +1,13 @@ @using Moonlight.Client.UI.Layouts @using Moonlight.Client.Services +@using Moonlight.Client.UI.Partials @inject ApplicationAssemblyService ApplicationAssemblyService \ No newline at end of file + AdditionalAssemblies="ApplicationAssemblyService.Assemblies"> + + + + \ No newline at end of file diff --git a/Moonlight.Client/UI/Components/WelcomeOverviewElement.razor b/Moonlight.Client/UI/Components/WelcomeOverviewElement.razor index 3d5bec1b..4add9855 100644 --- a/Moonlight.Client/UI/Components/WelcomeOverviewElement.razor +++ b/Moonlight.Client/UI/Components/WelcomeOverviewElement.razor @@ -1,4 +1,5 @@ -@using Microsoft.AspNetCore.Components.Authorization +@using System.Security.Claims +@using Microsoft.AspNetCore.Components.Authorization
@@ -18,7 +19,6 @@ protected override async Task OnInitializedAsync() { var identity = await AuthState; - var usernameClaim = identity.User.Claims.ToArray().First(x => x.Type == "username"); - Username = usernameClaim.Value; + Username = identity.User.FindFirst(ClaimTypes.Name)!.Value; } } diff --git a/Moonlight.Client/UI/Partials/AppSidebar.razor b/Moonlight.Client/UI/Partials/AppSidebar.razor index 428b25a1..1bfc892b 100644 --- a/Moonlight.Client/UI/Partials/AppSidebar.razor +++ b/Moonlight.Client/UI/Partials/AppSidebar.razor @@ -1,13 +1,11 @@ @using System.Security.Claims @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization -@using MoonCore.Blazor.FlyonUi.Auth @using Moonlight.Client.Interfaces @using Moonlight.Client.Models @using Moonlight.Client.UI.Layouts @inject NavigationManager Navigation -@inject AuthenticationStateManager AuthStateManager @inject IEnumerable SidebarItemProviders @inject IAuthorizationService AuthorizationService @@ -210,8 +208,8 @@ var authState = await AuthState; Identity = authState.User; - Username = Identity.Claims.First(x => x.Type == "username").Value; - Email = Identity.Claims.First(x => x.Type == "email").Value; + Username = Identity.FindFirst(ClaimTypes.Name)!.Value; + Email = Identity.FindFirst(ClaimTypes.Email)!.Value; var sidebarItems = new List(); @@ -260,8 +258,9 @@ return Task.CompletedTask; } - private async Task Logout() + private Task Logout() { - await AuthStateManager.Logout(); + Navigation.NavigateTo("/api/auth/logout", true); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/Moonlight.Client/UI/Partials/LoginSelector.razor b/Moonlight.Client/UI/Partials/LoginSelector.razor new file mode 100644 index 00000000..71b36a1e --- /dev/null +++ b/Moonlight.Client/UI/Partials/LoginSelector.razor @@ -0,0 +1,87 @@ +@using MoonCore.Helpers +@using Moonlight.Shared.Http.Responses.Auth + +@inject HttpApiClient ApiClient +@inject NavigationManager Navigation + +
+
+
+ + @if (ShowSelection) + { +
Login to MoonCore
+ +

Choose a login provider to start using the app

+ +
+ @foreach (var scheme in AuthSchemes) + { + var config = Configs.GetValueOrDefault(scheme.Identifier); + + if (config == null) // Ignore all schemes which have no ui configured + continue; + + + } +
+ } + else + { +
+ +
+ } +
+
+
+
+ +@code +{ + private AuthSchemeResponse[] AuthSchemes; + private Dictionary Configs = new(); + private bool ShowSelection = false; + + protected override void OnInitialized() + { + Configs["LocalAuth"] = new AuthSchemeConfig() + { + Color = "#7636e3", + IconUrl = "/placeholder.jpg" + }; + } + + private async Task Load(LazyLoader arg) + { + AuthSchemes = await ApiClient.GetJson( + "api/auth" + ); + + // If we only have one auth scheme available + // we want to auto redirect the user without + // showing the selection screen + + if (AuthSchemes.Length == 1) + await Start(AuthSchemes[0]); + else + ShowSelection = true; + } + + private Task Start(AuthSchemeResponse scheme) + { + Navigation.NavigateTo($"/api/auth/{scheme.Identifier}", true); + return Task.CompletedTask; + } + + record AuthSchemeConfig + { + public string Color { get; set; } + public string IconUrl { get; set; } + } +} \ No newline at end of file diff --git a/Moonlight.Client/UI/Views/Admin/Sys/Files.razor b/Moonlight.Client/UI/Views/Admin/Sys/Files.razor index 436a6d78..2d0c456e 100644 --- a/Moonlight.Client/UI/Views/Admin/Sys/Files.razor +++ b/Moonlight.Client/UI/Views/Admin/Sys/Files.razor @@ -4,6 +4,7 @@ @using MoonCore.Helpers @using Moonlight.Client.Implementations @using MoonCore.Blazor.FlyonUi.Files.Manager +@using MoonCore.Blazor.FlyonUi.Files.Manager.Operations @attribute [Authorize(Policy = "permissions:admin.system.overview")] @@ -13,7 +14,8 @@
- + @code { @@ -21,9 +23,21 @@ private static readonly long TransferChunkSize = ByteConverter.FromMegaBytes(20).Bytes; private static readonly long UploadLimit = ByteConverter.FromGigaBytes(20).Bytes; - + protected override void OnInitialized() { FsAccess = new SystemFsAccess(ApiClient); } + + private void OnConfigure(FileManagerOptions options) + { + options.AddMultiOperation(); + options.AddMultiOperation(); + options.AddMultiOperation(); + + options.AddSingleOperation(); + + options.AddToolbarOperation(); + options.AddToolbarOperation(); + } } diff --git a/Moonlight.Shared/Http/Requests/Auth/LoginCompleteRequest.cs b/Moonlight.Shared/Http/Requests/Auth/LoginCompleteRequest.cs deleted file mode 100644 index ba678c5f..00000000 --- a/Moonlight.Shared/Http/Requests/Auth/LoginCompleteRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Moonlight.Shared.Http.Requests.Auth; - -public class LoginCompleteRequest -{ - [Required(ErrorMessage = "You need to provide a code")] - public string Code { get; set; } -} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/Auth/AuthClaimResponse.cs b/Moonlight.Shared/Http/Responses/Auth/AuthClaimResponse.cs new file mode 100644 index 00000000..178c88a6 --- /dev/null +++ b/Moonlight.Shared/Http/Responses/Auth/AuthClaimResponse.cs @@ -0,0 +1,20 @@ +namespace Moonlight.Shared.Http.Responses.Auth; + +public class AuthClaimResponse +{ + // ReSharper disable once UnusedMember.Global + // Its used by the json serializer ^^ + public AuthClaimResponse() + { + + } + + public AuthClaimResponse(string type, string value) + { + Type = type; + Value = value; + } + + public string Type { get; set; } + public string Value { get; set; } +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/Auth/AuthSchemeResponse.cs b/Moonlight.Shared/Http/Responses/Auth/AuthSchemeResponse.cs new file mode 100644 index 00000000..079a08cd --- /dev/null +++ b/Moonlight.Shared/Http/Responses/Auth/AuthSchemeResponse.cs @@ -0,0 +1,7 @@ +namespace Moonlight.Shared.Http.Responses.Auth; + +public class AuthSchemeResponse +{ + public string DisplayName { get; set; } + public string Identifier { get; set; } +} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/Auth/CheckResponse.cs b/Moonlight.Shared/Http/Responses/Auth/CheckResponse.cs deleted file mode 100644 index 4073f3df..00000000 --- a/Moonlight.Shared/Http/Responses/Auth/CheckResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Moonlight.Shared.Http.Responses.Auth; - -public class CheckResponse -{ - public string Username { get; set; } - public string Email { get; set; } - public string[] Permissions { get; set; } -} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/Auth/LoginCompleteResponse.cs b/Moonlight.Shared/Http/Responses/Auth/LoginCompleteResponse.cs deleted file mode 100644 index f2ad9caa..00000000 --- a/Moonlight.Shared/Http/Responses/Auth/LoginCompleteResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Moonlight.Shared.Http.Responses.Auth; - -public class LoginCompleteResponse -{ - public string AccessToken { get; set; } -} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/Auth/LoginStartResponse.cs b/Moonlight.Shared/Http/Responses/Auth/LoginStartResponse.cs deleted file mode 100644 index 2130beef..00000000 --- a/Moonlight.Shared/Http/Responses/Auth/LoginStartResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Moonlight.Shared.Http.Responses.Auth; - -public class LoginStartResponse -{ - public string Url { get; set; } -} \ No newline at end of file diff --git a/Moonlight.Shared/Http/Responses/OAuth2/OAuth2HandleResponse.cs b/Moonlight.Shared/Http/Responses/OAuth2/OAuth2HandleResponse.cs deleted file mode 100644 index a27c9265..00000000 --- a/Moonlight.Shared/Http/Responses/OAuth2/OAuth2HandleResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Moonlight.Shared.Http.Responses.OAuth2; - -public class OAuth2HandleResponse -{ - public int UserId { get; set; } -} \ No newline at end of file