using System.Diagnostics.CodeAnalysis; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Helpers; using Iceshrimp.Backend.Core.Middleware; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Pages.OAuth; [HideRequestDuration] public class AuthorizeModel(DatabaseContext db) : PageModel { public OauthApp App = null!; public List Scopes = []; public OauthToken? Token; public List AuthenticatedUsers = []; [FromQuery(Name = "response_type")] public string ResponseType { get; set; } = null!; [FromQuery(Name = "client_id")] public string ClientId { get; set; } = null!; [FromQuery(Name = "redirect_uri")] public string RedirectUri { get; set; } = null!; [FromQuery(Name = "force_login")] public bool ForceLogin { get; set; } = false; [FromQuery(Name = "lang")] public string? Language { get; set; } [FromQuery(Name = "scope")] public string Scope { get => string.Join(' ', Scopes); [MemberNotNull(nameof(Scopes))] set => Scopes = value.Split(' ').ToList(); } public LoginData? TwoFactorFormData; public async Task OnGet() { if (ResponseType == null || ClientId == null || RedirectUri == null) throw GracefulException.BadRequest("Required parameters are missing or invalid"); App = await db.OauthApps.FirstOrDefaultAsync(p => p.ClientId == ClientId.Replace(' ', '+')) ?? throw GracefulException.BadRequest("Invalid client_id"); if (MastodonOauthHelpers.ExpandScopes(Scopes).Except(MastodonOauthHelpers.ExpandScopes(App.Scopes)).Any()) throw GracefulException.BadRequest("Cannot request more scopes than app"); if (ResponseType != "code") throw GracefulException.BadRequest("Invalid response_type"); if (!App.RedirectUris.Contains(RedirectUri)) throw GracefulException.BadRequest("Cannot request redirect_uri not sent during app registration"); if (Request.Cookies.TryGetValue("sessions", out var sessions)) { var tokens = sessions.Split(','); AuthenticatedUsers = await db.Sessions .Where(p => tokens.Contains(p.Token) && p.Active) .Select(p => p.User) .ToListAsync(); } } public async Task OnPost( [FromForm] string? username, [FromForm] string? password, [FromForm] string? totp, [FromForm] string? userId, [FromForm] bool supportsHtmlFormatting, [FromForm] bool autoDetectQuotes, [FromForm] bool isPleroma, [FromForm] bool supportsInlineMedia ) { // Validate query parameters & populate model first await OnGet(); var user = AuthenticatedUsers.FirstOrDefault(p => p.Id == userId); if (user == null) { Exception Forbidden() => GracefulException.Forbidden("Invalid username or password"); if (username == null || password == null) throw Forbidden(); user = await db.Users.FirstOrDefaultAsync(p => p.IsLocalUser && p.UsernameLower == username.ToLowerInvariant()) ?? throw Forbidden(); if (user.IsSystemUser) throw GracefulException.BadRequest("Cannot log in as system user"); var userSettings = await db.UserSettings.FirstOrDefaultAsync(p => p.User == user); if (userSettings?.Password == null) throw Forbidden(); if (AuthHelpers.ComparePassword(password, userSettings.Password) == false) throw Forbidden(); if (userSettings.TwoFactorEnabled) { if (!string.IsNullOrWhiteSpace(totp)) { if (userSettings.TwoFactorSecret == null) throw new Exception("2FA is enabled but secret is null"); if (!TotpHelper.Validate(userSettings.TwoFactorSecret, totp)) { SetTwoFactorFormData(); return; } } else { SetTwoFactorFormData(); return; } } } var token = new OauthToken { Id = IdHelpers.GenerateSnowflakeId(), Active = false, Code = CryptographyHelpers.GenerateRandomString(32), Token = CryptographyHelpers.GenerateRandomString(32), App = App, User = user, CreatedAt = DateTime.UtcNow, Scopes = Scopes, RedirectUri = RedirectUri, AutoDetectQuotes = autoDetectQuotes, SupportsHtmlFormatting = supportsHtmlFormatting, IsPleroma = isPleroma, SupportsInlineMedia = supportsInlineMedia, }; await db.AddAsync(token); await db.SaveChangesAsync(); Token = token; return; void SetTwoFactorFormData() { TwoFactorFormData = new LoginData { Username = username, Password = password, AutoDetectQuotes = autoDetectQuotes, SupportsHtmlFormatting = supportsHtmlFormatting, IsPleroma = isPleroma, SupportsInlineMedia = supportsInlineMedia, }; Response.Headers.CacheControl = "private, no-store, no-cache"; Response.Headers.Pragma = "no-cache"; } } public class LoginData { public required string Username; public required string Password; public required bool SupportsHtmlFormatting; public required bool AutoDetectQuotes; public required bool IsPleroma; public required bool SupportsInlineMedia; } }