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.Permissions; using Moonlight.ApiServer.Implementations.LocalAuth; using Moonlight.ApiServer.Services; namespace Moonlight.ApiServer.Startup; public partial class Startup { private Task RegisterAuth() { WebApplicationBuilder.Services .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 => { var headers = context.Request.Headers; // For regular api calls if (headers.ContainsKey("Authorization")) return "ApiKey"; // For websocket requests which cannot use the Authorization header if (headers.Upgrade == "websocket" && headers.Connection == "Upgrade" && context.Request.Query.ContainsKey("access_token")) return "ApiKey"; // Regular user traffic/auth return "Session"; }; }) .AddJwtBearer("ApiKey", null, 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 }; 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"); }, OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; if (!string.IsNullOrEmpty(accessToken)) context.Token = accessToken; return Task.CompletedTask; } }; }) .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.AddAuthorization(); WebApplicationBuilder.Services.AddAuthorizationPermissions(options => { options.ClaimName = "Permissions"; options.Prefix = "permissions:"; }); WebApplicationBuilder.Services.AddScoped(); 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(); return Task.CompletedTask; } private Task UseAuth() { WebApplication.UseAuthentication(); WebApplication.UseAuthorization(); return Task.CompletedTask; } }