From bf5a7444995b48578ca45558542a10a421b76769 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Tue, 4 Feb 2025 17:09:07 +0100 Subject: [PATCH] Starting updating mooncore dependency usage --- .../Configuration/AppConfiguration.cs | 26 +- Moonlight.ApiServer/Database/Entities/User.cs | 12 +- .../Admin/ApiKeys/ApiKeysController.cs | 2 +- .../Http/Controllers/Auth/AuthController.cs | 324 +++++++----------- .../Http/Controllers/FrontendController.cs | 4 +- .../Http/Controllers/OAuth2/Login.razor | 67 ++++ .../Controllers/OAuth2/OAuth2Controller.cs | 290 ++++++++++++++++ .../Http/Controllers/OAuth2/Register.razor | 72 ++++ .../OAuth2/LocalOAuth2Provider.cs | 65 ---- .../Moonlight.ApiServer.csproj | 4 +- Moonlight.ApiServer/Startup.cs | 102 +++--- .../AuthenticationUiHandler.cs | 21 -- Moonlight.Client/Moonlight.Client.csproj | 4 +- Moonlight.Client/Services/DownloadService.cs | 34 -- Moonlight.Client/Services/IdentityService.cs | 83 ----- .../Services/RemoteAuthStateManager.cs | 124 +++++++ Moonlight.Client/Startup.cs | 60 ++-- Moonlight.Client/Styles/mappings/mooncore.map | 34 +- Moonlight.Client/UI/App.razor | 27 +- Moonlight.Client/UI/Forms/DateComponent.razor | 3 - Moonlight.Client/UI/Layouts/MainLayout.razor | 13 +- Moonlight.Client/UI/Partials/AppHeader.razor | 48 +-- Moonlight.Client/UI/Partials/AppSidebar.razor | 106 +++--- .../UI/Views/Admin/Api/Index.razor | 4 +- .../UI/Views/Admin/Users/Create.razor | 69 ++++ .../UI/Views/Admin/Users/Index.razor | 100 +++--- .../UI/Views/Admin/Users/Update.razor | 69 ++++ Moonlight.Client/UI/Views/Index.razor | 22 +- Moonlight.Client/_Imports.razor | 2 - ...ndleRequest.cs => LoginCompleteRequest.cs} | 4 +- .../Http/Requests/Auth/RefreshRequest.cs | 9 - .../Http/Responses/Auth/CheckResponse.cs | 4 +- .../Responses/Auth/LoginCompleteResponse.cs | 6 + ...StartResponse.cs => LoginStartResponse.cs} | 4 +- .../Responses/Auth/OAuth2HandleResponse.cs | 8 - .../Http/Responses/Auth/RefreshResponse.cs | 8 - .../Http/Responses/OAuth2/InfoResponse.cs | 7 - .../Responses/OAuth2/OAuth2HandleResponse.cs | 6 + 38 files changed, 1099 insertions(+), 748 deletions(-) create mode 100644 Moonlight.ApiServer/Http/Controllers/OAuth2/Login.razor create mode 100644 Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs create mode 100644 Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor delete mode 100644 Moonlight.ApiServer/Implementations/OAuth2/LocalOAuth2Provider.cs delete mode 100644 Moonlight.Client/Implementations/AuthenticationUiHandler.cs delete mode 100644 Moonlight.Client/Services/DownloadService.cs delete mode 100644 Moonlight.Client/Services/IdentityService.cs create mode 100644 Moonlight.Client/Services/RemoteAuthStateManager.cs delete mode 100644 Moonlight.Client/UI/Forms/DateComponent.razor create mode 100644 Moonlight.Client/UI/Views/Admin/Users/Create.razor create mode 100644 Moonlight.Client/UI/Views/Admin/Users/Update.razor rename Moonlight.Shared/Http/Requests/Auth/{OAuth2HandleRequest.cs => LoginCompleteRequest.cs} (56%) delete mode 100644 Moonlight.Shared/Http/Requests/Auth/RefreshRequest.cs create mode 100644 Moonlight.Shared/Http/Responses/Auth/LoginCompleteResponse.cs rename Moonlight.Shared/Http/Responses/Auth/{OAuth2StartResponse.cs => LoginStartResponse.cs} (84%) delete mode 100644 Moonlight.Shared/Http/Responses/Auth/OAuth2HandleResponse.cs delete mode 100644 Moonlight.Shared/Http/Responses/Auth/RefreshResponse.cs delete mode 100644 Moonlight.Shared/Http/Responses/OAuth2/InfoResponse.cs create mode 100644 Moonlight.Shared/Http/Responses/OAuth2/OAuth2HandleResponse.cs diff --git a/Moonlight.ApiServer/Configuration/AppConfiguration.cs b/Moonlight.ApiServer/Configuration/AppConfiguration.cs index f800332b..9a63565e 100644 --- a/Moonlight.ApiServer/Configuration/AppConfiguration.cs +++ b/Moonlight.ApiServer/Configuration/AppConfiguration.cs @@ -29,34 +29,18 @@ public class AppConfiguration public class AuthenticationConfig { - public string AccessSecret { get; set; } = Formatter.GenerateString(32); - public string RefreshSecret { get; set; } = Formatter.GenerateString(32); - public int AccessDuration { get; set; } = 60; - public int RefreshDuration { get; set; } = 3600; - + public string Secret { get; set; } = Formatter.GenerateString(32); + public int TokenDuration { get; set; } = 3600; + public OAuth2Data OAuth2 { get; set; } = new(); - public bool UseLocalOAuth2 { get; set; } = true; - - public LocalOAuth2Data LocalOAuth2 { get; set; } = new(); - - public class LocalOAuth2Data - { - public string AccessSecret { get; set; } = Formatter.GenerateString(32); - public string RefreshSecret { get; set; } = Formatter.GenerateString(32); - public string CodeSecret { get; set; } = Formatter.GenerateString(32); - - public int AccessTokenDuration { get; set; } = 60; - public int RefreshTokenDuration { get; set; } = 3600; - } public class 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? AuthorizationUri { get; set; } + public string? AuthorizationEndpoint { get; set; } public string? AuthorizationRedirect { get; set; } - public string? AccessEndpoint { get; set; } - public string? RefreshEndpoint { get; set; } } } diff --git a/Moonlight.ApiServer/Database/Entities/User.cs b/Moonlight.ApiServer/Database/Entities/User.cs index 2ecfaa5f..91397b0d 100644 --- a/Moonlight.ApiServer/Database/Entities/User.cs +++ b/Moonlight.ApiServer/Database/Entities/User.cs @@ -1,8 +1,6 @@ -using MoonCore.Extended.OAuth2.Consumer; +namespace Moonlight.ApiServer.Database.Entities; -namespace Moonlight.ApiServer.Database.Entities; - -public class User : IUserModel +public class User { public int Id { get; set; } @@ -10,10 +8,6 @@ public class User : IUserModel public string Email { get; set; } public string Password { get; set; } - public DateTime TokenValidTimestamp { get; set; } = DateTime.UtcNow; + public DateTime TokenValidTimestamp { get; set; } = DateTime.MinValue; public string PermissionsJson { get; set; } = "[]"; - - public string AccessToken { get; set; } = ""; - public string RefreshToken { get; set; } = ""; - public DateTime RefreshTimestamp { get; set; } = DateTime.UtcNow; } \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs b/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs index 7994b2c2..d65e8f1f 100644 --- a/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Admin/ApiKeys/ApiKeysController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; -using MoonCore.Attributes; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Helpers; +using MoonCore.Extended.PermFilter; using MoonCore.Helpers; using MoonCore.Models; using Moonlight.ApiServer.Database.Entities; diff --git a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs index 28de66d3..59d8a7e2 100644 --- a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs @@ -1,8 +1,18 @@ -using Microsoft.AspNetCore.Mvc; -using MoonCore.Attributes; -using MoonCore.Authentication; +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using MoonCore.Exceptions; +using MoonCore.Extended.Abstractions; +using MoonCore.Helpers; +using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database.Entities; +using Moonlight.Shared.Http.Requests.Auth; using Moonlight.Shared.Http.Responses.Auth; +using Moonlight.Shared.Http.Responses.OAuth2; namespace Moonlight.ApiServer.Http.Controllers.Auth; @@ -10,206 +20,138 @@ namespace Moonlight.ApiServer.Http.Controllers.Auth; [Route("api/auth")] public class AuthController : Controller { - /* - private readonly OAuth2Service OAuth2Service; - private readonly TokenHelper TokenHelper; - private readonly DatabaseRepository UserRepository; - private readonly ILogger Logger; private readonly AppConfiguration Configuration; - private readonly IOAuth2Provider[] OAuth2Providers; - private readonly IAuthInterceptor[] AuthInterceptors; + private readonly ILogger Logger; + private readonly DatabaseRepository UserRepository; public AuthController( - OAuth2Service oAuth2Service, - TokenHelper tokenHelper, - DatabaseRepository userRepository, + AppConfiguration configuration, ILogger logger, - IOAuth2Provider[] oAuth2Providers, - IAuthInterceptor[] authInterceptors, - AppConfiguration configuration) + DatabaseRepository userRepository + ) { - OAuth2Service = oAuth2Service; - TokenHelper = tokenHelper; - UserRepository = userRepository; - Logger = logger; - OAuth2Providers = oAuth2Providers; - AuthInterceptors = authInterceptors; Configuration = configuration; + Logger = logger; + UserRepository = userRepository; } - [HttpGet] - public async Task Start() + [AllowAnonymous] + [HttpGet("start")] + public Task Start() { - var data = await OAuth2Service.StartAuthorizing(); - - return Mapper.Map(data); - } - - [HttpPost] - public async Task Handle([FromBody] OAuth2HandleRequest request) - { - var accessData = await OAuth2Service.RequestAccess(request.Code); - - // Find oauth2 provider - var provider = OAuth2Providers.FirstOrDefault(); - - if (provider == null) - throw new HttpApiException("No oauth2 provider has been registered", 500); - - // Sync user from oauth2 provider - var user = await provider.Sync(HttpContext.RequestServices, accessData.AccessToken); - - if (user == null) - throw new HttpApiException("The oauth2 provider was unable to authenticate you", 401); - - // Allow plugins to intercept access calls - if (AuthInterceptors.Any(interceptor => !interceptor.AllowAccess(user, HttpContext.RequestServices))) - throw new HttpApiException("Unable to get access token", 401); - - // Save oauth2 refresh and access tokens for later use (re-authentication etc.). - // Fetch user model in current db context, just in case the oauth2 provider - // uses a different db context or smth - - var userModel = UserRepository - .Get() - .First(x => x.Id == user.Id); - - userModel.AccessToken = accessData.AccessToken; - userModel.RefreshToken = accessData.RefreshToken; - userModel.RefreshTimestamp = DateTime.UtcNow.AddSeconds(accessData.ExpiresIn); - - UserRepository.Update(userModel); - - // Generate local token-pair for the authentication - // between client and the api server - - var authConfig = Configuration.Authentication; - - var tokenPair = TokenHelper.GeneratePair( - authConfig.AccessSecret, - authConfig.RefreshSecret, - data => { data.Add("userId", user.Id); }, - authConfig.AccessDuration, - authConfig.RefreshDuration - ); - - // Authentication finished. Return data to client - - return new OAuth2HandleResponse() + var response = new LoginStartResponse() { - AccessToken = tokenPair.AccessToken, - RefreshToken = tokenPair.RefreshToken, - ExpiresAt = DateTime.UtcNow.AddSeconds(authConfig.AccessDuration) - }; - } - - [HttpPost("refresh")] - public async Task Refresh([FromBody] RefreshRequest request) - { - var authConfig = Configuration.Authentication; - - var tokenPair = TokenHelper.RefreshPair( - request.RefreshToken, - authConfig.AccessSecret, - authConfig.RefreshSecret, - (refreshData, newData) - => ProcessRefreshData(refreshData, newData, HttpContext.RequestServices), - authConfig.AccessDuration, - authConfig.RefreshDuration - ); - - // Handle refresh error - if (!tokenPair.HasValue) - throw new HttpApiException("Unable to refresh token", 401); - - // Return data - return new RefreshResponse() - { - AccessToken = tokenPair.Value.AccessToken, - RefreshToken = tokenPair.Value.RefreshToken, - ExpiresAt = DateTime.UtcNow.AddSeconds(authConfig.AccessDuration) - }; - } - - private bool ProcessRefreshData(Dictionary refreshTokenData, Dictionary newData, IServiceProvider serviceProvider) - { - // Find oauth2 provider - var provider = OAuth2Providers.FirstOrDefault(); - - if (provider == null) - throw new HttpApiException("No oauth2 provider has been registered", 500); - - // Check if the userId is present in the refresh token - if (!refreshTokenData.TryGetValue("userId", out var userIdStr) || !userIdStr.TryGetInt32(out var userId)) - return false; - - // Load user from database if existent - var user = UserRepository - .Get() - .FirstOrDefault(x => x.Id == userId); - - if (user == null) - return false; - - // Allow plugins to intercept the refresh call - if (AuthInterceptors.Any(interceptor => !interceptor.AllowRefresh(user, serviceProvider))) - return false; - - // Check if it's time to resync with the oauth2 provider - if (DateTime.UtcNow >= user.RefreshTimestamp) - { - try - { - // It's time to refresh the access to the external oauth2 provider - var refreshData = OAuth2Service.RefreshAccess(user.RefreshToken).Result; - - // Sync user with oauth2 provider - var syncedUser = provider.Sync(serviceProvider, refreshData.AccessToken).Result; - - if (syncedUser == null) // User sync has failed. No refresh allowed - return false; - - // Save oauth2 refresh and access tokens for later use (re-authentication etc.). - // Fetch user model in current db context, just in case the oauth2 provider - // uses a different db context or smth - - var userModel = UserRepository - .Get() - .First(x => x.Id == syncedUser.Id); - - userModel.AccessToken = refreshData.AccessToken; - userModel.RefreshToken = refreshData.RefreshToken; - userModel.RefreshTimestamp = DateTime.UtcNow.AddSeconds(refreshData.ExpiresIn); - - UserRepository.Update(userModel); - } - catch (Exception e) - { - // We are handling this error more softly, because it will occur when a user hasn't logged in a long period of time - Logger.LogDebug("An error occured while refreshing external oauth2 access: {e}", e); - return false; - } - } - - // All checks have passed, allow refresh - newData.Add("userId", user.Id); - return true; - }*/ - - [HttpGet("check")] - [RequirePermission("meta.authenticated")] - public Task Check() - { - var permClaim = (HttpContext.User as PermClaimsPrinciple)!; - var user = (User)permClaim.IdentityModel; - - var response = new CheckResponse() - { - Email = user.Email, - Username = user.Username, - Permissions = permClaim.Permissions + ClientId = Configuration.Authentication.OAuth2.ClientId, + RedirectUri = Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl, + Endpoint = Configuration.Authentication.OAuth2.AuthorizationEndpoint ?? Configuration.PublicUrl + "/oauth2/authorize" }; return Task.FromResult(response); } + + [AllowAnonymous] + [HttpPost("complete")] + public async Task Complete([FromBody] LoginCompleteRequest request) + { + // TODO: Make modular + + // Create http client to call the auth provider + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(Configuration.PublicUrl); + httpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {Configuration.Authentication.OAuth2.ClientSecret}"); + + var httpApiClient = new HttpApiClient(httpClient); + + // Call the auth provider + OAuth2HandleResponse handleData; + + try + { + handleData = await httpApiClient.PostJson("oauth2/handle", new FormUrlEncodedContent( + [ + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("code", request.Code), + new KeyValuePair("redirect_uri", Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl), + 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); + } + + // Handle the returned data + var userId = handleData.UserId; + + var user = await UserRepository + .Get() + .FirstOrDefaultAsync(x => x.Id == userId); + + if (user == null) + throw new HttpApiException("Unable to load user data", 500); + + // + var permissions = JsonSerializer.Deserialize(user.PermissionsJson) ?? []; + + // Generate token + var securityTokenDescriptor = new SecurityTokenDescriptor() + { + Expires = DateTime.Now.AddDays(10), + IssuedAt = DateTime.Now, + NotBefore = DateTime.Now.AddMinutes(-1), + Claims = new Dictionary() + { + { + "userId", + user.Id + }, + { + "permissions", + string.Join(";", permissions) + } + }, + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(Configuration.Authentication.Secret) + ), + SecurityAlgorithms.HmacSha256 + ), + Issuer = Configuration.PublicUrl, + Audience = Configuration.PublicUrl + }; + + var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); + var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor); + + var jwt = jwtSecurityTokenHandler.WriteToken(securityToken); + + return new() + { + AccessToken = jwt + }; + } + + [Authorize] + [HttpGet("check")] + public async Task Check() + { + var userIdClaim = User.Claims.First(x => x.Type == "userId"); + var userId = int.Parse(userIdClaim.Value); + var user = await UserRepository.Get().FirstAsync(x => x.Id == userId); + + var permissions = JsonSerializer.Deserialize(user.PermissionsJson) ?? []; + + return new() + { + Email = user.Email, + Username = user.Username, + Permissions = string.Join(";", permissions) + }; + } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/FrontendController.cs b/Moonlight.ApiServer/Http/Controllers/FrontendController.cs index b9bd76d1..4247db31 100644 --- a/Moonlight.ApiServer/Http/Controllers/FrontendController.cs +++ b/Moonlight.ApiServer/Http/Controllers/FrontendController.cs @@ -25,7 +25,7 @@ public class FrontendController : Controller } [HttpGet("frontend.json")] - public async Task GetConfiguration() + public Task GetConfiguration() { var configuration = new FrontendConfiguration() { @@ -39,7 +39,7 @@ public class FrontendController : Controller configuration.Scripts = AssetService.GetJavascriptAssets(); - return configuration; + return Task.FromResult(configuration); } [HttpGet("plugins/{assemblyName}")] // TODO: Test this diff --git a/Moonlight.ApiServer/Http/Controllers/OAuth2/Login.razor b/Moonlight.ApiServer/Http/Controllers/OAuth2/Login.razor new file mode 100644 index 00000000..7ce293ad --- /dev/null +++ b/Moonlight.ApiServer/Http/Controllers/OAuth2/Login.razor @@ -0,0 +1,67 @@ + + + Login into your account + + + + + + +
+
+ Your Company +

Login into your account

+
+ +
+
+ + @if (!string.IsNullOrEmpty(ErrorMessage)) + { +
+ @ErrorMessage +
+ } + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +

+ No account? + + Register + +

+
+
+ + + + +@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 new file mode 100644 index 00000000..58d44c61 --- /dev/null +++ b/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs @@ -0,0 +1,290 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Authorization; +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 class OAuth2Controller : Controller +{ + private readonly AppConfiguration Configuration; + private readonly DatabaseRepository UserRepository; + + public OAuth2Controller(AppConfiguration configuration, DatabaseRepository userRepository) + { + Configuration = configuration; + UserRepository = userRepository; + } + + [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" + ) + { + var requiredRedirectUri = Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl; + + if (Configuration.Authentication.OAuth2.ClientId != clientId || + requiredRedirectUri != redirectUri || + responseType != "code") + { + throw new HttpApiException("Invalid oauth2 request", 400); + } + + Response.StatusCode = 200; + + if (view == "register") + { + var html = await ComponentHelper.RenderComponent(HttpContext.RequestServices, parameters => + { + parameters.Add("ClientId", clientId); + parameters.Add("RedirectUri", redirectUri); + parameters.Add("ResponseType", responseType); + }); + + await Response.WriteAsync(html); + } + else + { + var html = await ComponentHelper.RenderComponent(HttpContext.RequestServices, parameters => + { + parameters.Add("ClientId", clientId); + parameters.Add("RedirectUri", redirectUri); + parameters.Add("ResponseType", responseType); + }); + + await Response.WriteAsync(html); + } + } + + [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")] string email, + [FromForm(Name = "password")] string password, + [FromForm(Name = "username")] string username = "", + [FromQuery(Name = "view")] string view = "login" + ) + { + var requiredRedirectUri = Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl; + + if (Configuration.Authentication.OAuth2.ClientId != clientId || + requiredRedirectUri != redirectUri || + 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}"); + return; + } + else + { + var user = await Login(email, password); + var code = await GenerateCode(user); + + Response.Redirect($"{redirectUri}?code={code}"); + return; + } + } + catch (HttpApiException e) + { + errorMessage = e.Title; + } + + Response.StatusCode = 200; + + if (view == "register") + { + var html = await ComponentHelper.RenderComponent(HttpContext.RequestServices, parameters => + { + parameters.Add("ClientId", clientId); + parameters.Add("RedirectUri", redirectUri); + parameters.Add("ResponseType", responseType); + parameters.Add("ErrorMessage", errorMessage!); + }); + + await Response.WriteAsync(html); + } + else + { + var html = await ComponentHelper.RenderComponent(HttpContext.RequestServices, parameters => + { + parameters.Add("ClientId", clientId); + parameters.Add("RedirectUri", redirectUri); + parameters.Add("ResponseType", responseType); + parameters.Add("ErrorMessage", errorMessage!); + }); + + await Response.WriteAsync(html); + } + } + + [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 + ) + { + // 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 != (Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl)) + 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.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 async 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.Secret) + ), + SecurityAlgorithms.HmacSha256 + ) + }; + + var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); + var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor); + + return 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); + + var user = new User() + { + Username = username, + Email = email, + Password = HashHelper.Hash(password) + }; + + var finalUser = await UserRepository.Add(user); + + return finalUser; + } + + 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; + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor b/Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor new file mode 100644 index 00000000..4b9de47b --- /dev/null +++ b/Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor @@ -0,0 +1,72 @@ + + + Register a new account + + + + + + +
+
+ Your Company +

Create your account

+
+ +
+
+ + @if (!string.IsNullOrEmpty(ErrorMessage)) + { +
+ @ErrorMessage +
+ } + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +

+ Already registered? + Login +

+
+
+ + + + +@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/Implementations/OAuth2/LocalOAuth2Provider.cs b/Moonlight.ApiServer/Implementations/OAuth2/LocalOAuth2Provider.cs deleted file mode 100644 index 8c48018e..00000000 --- a/Moonlight.ApiServer/Implementations/OAuth2/LocalOAuth2Provider.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using MoonCore.Exceptions; -using MoonCore.Extended.Abstractions; -using MoonCore.Extended.Helpers; -using MoonCore.Extended.OAuth2.LocalProvider; -using Moonlight.ApiServer.Database.Entities; - -namespace Moonlight.ApiServer.Implementations.OAuth2; - -public class LocalOAuth2Provider : ILocalProviderImplementation -{ - private readonly DatabaseRepository UserRepository; - - public LocalOAuth2Provider(DatabaseRepository userRepository) - { - UserRepository = userRepository; - } - - public async Task SaveChanges(User model) - { - await UserRepository.Update(model); - } - - public async Task LoadById(int id) - { - var res = await UserRepository - .Get() - .FirstOrDefaultAsync(x => x.Id == id); - - return res; - } - - public Task Login(string email, string password) - { - var user = UserRepository.Get().FirstOrDefault(x => x.Email == email); - - if (user == null) - throw new HttpApiException("Invalid email or password", 400); - - if(!HashHelper.Verify(password, user.Password)) - throw new HttpApiException("Invalid email or password", 400); - - return Task.FromResult(user); - } - - public async Task Register(string username, string email, string password) - { - if (UserRepository.Get().Any(x => x.Username == username)) - throw new HttpApiException("A user with that username already exists", 400); - - if (UserRepository.Get().Any(x => x.Email == email)) - throw new HttpApiException("A user with that email address already exists", 400); - - var user = new User() - { - Username = username, - Email = email, - Password = HashHelper.Hash(password) - }; - - var finalUser = await UserRepository.Add(user); - - return finalUser; - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index 5c523110..0c9dff2a 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -24,8 +24,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index 10cc8306..72b82a1e 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -1,14 +1,14 @@ using System.Reflection; +using System.Text; using System.Text.Json; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; using MoonCore.Configuration; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Extensions; using MoonCore.Extended.Helpers; -using MoonCore.Extended.OAuth2.Consumer; -using MoonCore.Extended.OAuth2.Consumer.Extensions; -using MoonCore.Extended.OAuth2.LocalProvider; -using MoonCore.Extended.OAuth2.LocalProvider.Extensions; -using MoonCore.Extended.OAuth2.LocalProvider.Implementations; +using MoonCore.Extended.JwtInvalidation; using MoonCore.Extensions; using MoonCore.Helpers; using MoonCore.PluginFramework.Extensions; @@ -17,8 +17,6 @@ using MoonCore.Services; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Helpers; -using Moonlight.ApiServer.Http.Middleware; -using Moonlight.ApiServer.Implementations.OAuth2; using Moonlight.ApiServer.Interfaces.Auth; using Moonlight.ApiServer.Interfaces.OAuth2; using Moonlight.ApiServer.Interfaces.Startup; @@ -78,7 +76,7 @@ public class Startup await RegisterLogging(); await RegisterBase(); await RegisterDatabase(); - await RegisterOAuth2(); + await RegisterAuth(); await RegisterCaching(); await HookPluginBuild(); await HandleConfigureArguments(); @@ -90,13 +88,11 @@ public class Startup await PrepareDatabase(); await UseBase(); - await UseOAuth2(); - await UseBaseMiddleware(); + await UseAuth(); await HookPluginConfigure(); await UsePluginAssets(); await MapBase(); - await MapOAuth2(); await HookPluginEndpoints(); await WebApplication.RunAsync(); @@ -240,14 +236,6 @@ public class Startup return Task.CompletedTask; } - private Task UseBaseMiddleware() - { - WebApplication.UseMiddleware(); - WebApplication.UseMiddleware(); - - return Task.CompletedTask; - } - private Task MapBase() { WebApplication.MapControllers(); @@ -593,50 +581,56 @@ public class Startup #endregion - #region OAuth2 + #region Authentication & Authorisation - private Task RegisterOAuth2() + private Task RegisterAuth() { - WebApplicationBuilder.Services.AddOAuth2Authentication(configuration => + WebApplicationBuilder.Services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new() + { + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes( + Configuration.Authentication.Secret + )), + ValidateIssuerSigningKey = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero, + ValidateAudience = true, + ValidAudience = Configuration.PublicUrl, + ValidateIssuer = true, + ValidIssuer = Configuration.PublicUrl + }; + }); + + WebApplicationBuilder.Services.AddJwtInvalidation(options => { - configuration.AccessSecret = Configuration.Authentication.AccessSecret; - configuration.RefreshSecret = Configuration.Authentication.RefreshSecret; - configuration.RefreshDuration = TimeSpan.FromSeconds(Configuration.Authentication.RefreshDuration); - configuration.RefreshInterval = TimeSpan.FromSeconds(Configuration.Authentication.AccessDuration); - configuration.ClientId = Configuration.Authentication.OAuth2.ClientId; - configuration.ClientSecret = Configuration.Authentication.OAuth2.ClientSecret; - configuration.AuthorizeEndpoint = Configuration.PublicUrl + "/api/_auth/oauth2/authorize"; - configuration.RedirectUri = Configuration.PublicUrl; + options.InvalidateTimeProvider = async (provider, principal) => + { + var userIdClaim = principal.Claims.First(x => x.Type == "userId"); + var userId = int.Parse(userIdClaim.Value); + + var userRepository = provider.GetRequiredService>(); + var user = await userRepository.Get().FirstAsync(x => x.Id == userId); + + return user.TokenValidTimestamp; + }; }); - WebApplicationBuilder.Services.AddScoped, LocalOAuth2Provider>(); - - if (!Configuration.Authentication.UseLocalOAuth2) - return Task.CompletedTask; - - WebApplicationBuilder.Services.AddLocalOAuth2Provider(Configuration.PublicUrl); - WebApplicationBuilder.Services.AddScoped, LocalOAuth2Provider>(); - WebApplicationBuilder.Services.AddScoped, LocalOAuth2Provider>(); - + WebApplicationBuilder.Services.AddAuthorization(); + return Task.CompletedTask; } - private Task UseOAuth2() + private Task UseAuth() { - WebApplication.UseOAuth2Authentication(); - WebApplication.UseMiddleware(); - - return Task.CompletedTask; - } - - private Task MapOAuth2() - { - WebApplication.MapOAuth2Authentication(); - - if (!Configuration.Authentication.UseLocalOAuth2) - return Task.CompletedTask; - - WebApplication.MapLocalOAuth2Provider(); + WebApplication.UseAuthentication(); + + WebApplication.UseJwtInvalidation(); + + WebApplication.UseAuthorization(); + return Task.CompletedTask; } diff --git a/Moonlight.Client/Implementations/AuthenticationUiHandler.cs b/Moonlight.Client/Implementations/AuthenticationUiHandler.cs deleted file mode 100644 index df4afc3a..00000000 --- a/Moonlight.Client/Implementations/AuthenticationUiHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Components; -using Moonlight.Client.Interfaces; -using Moonlight.Client.Services; - -namespace Moonlight.Client.Implementations; - -public class AuthenticationUiHandler : IAppLoader, IAppScreen -{ - public int Priority => 0; - - public Task ShouldRender(IServiceProvider serviceProvider) - => Task.FromResult(false); - - public RenderFragment Render() => throw new NotImplementedException(); - - public async Task Load(IServiceProvider serviceProvider) - { - var identityService = serviceProvider.GetRequiredService(); - await identityService.Check(); - } -} \ No newline at end of file diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index 6ce46174..aa5e9a4c 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -24,10 +24,10 @@ - + - + -
- - + @@ -76,8 +55,17 @@ @code { [Parameter] public MainLayout Layout { get; set; } + [CascadingParameter] public Task AuthState { get; set; } private bool ShowProfileNav = false; + private string Username; + + protected override async Task OnInitializedAsync() + { + var identity = await AuthState; + var usernameClaim = identity.User.Claims.ToArray().First(x => x.Type == "username"); + Username = usernameClaim.Value; + } protected override Task OnAfterRenderAsync(bool firstRender) { @@ -109,11 +97,5 @@ } private async Task Logout() - { - await IdentityService.Logout(); - await ToastService.Info("Successfully logged out"); - - //await Layout.Load(); - Navigation.NavigateTo(Navigation.Uri, true); - } + => await AuthStateManager.Logout(); } diff --git a/Moonlight.Client/UI/Partials/AppSidebar.razor b/Moonlight.Client/UI/Partials/AppSidebar.razor index 471f16c1..3bf1ec9e 100644 --- a/Moonlight.Client/UI/Partials/AppSidebar.razor +++ b/Moonlight.Client/UI/Partials/AppSidebar.razor @@ -4,40 +4,19 @@ @using Moonlight.Client.UI.Layouts @inject NavigationManager Navigation -@inject IdentityService IdentityService @inject ISidebarItemProvider[] SidebarItemProviders @{ var url = new Uri(Navigation.Uri); } -