using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using MongoDB.Driver; using MongoDB.Entities; using OpenIddict.Abstractions; using OpenIddict.Server; using OpenIddict.Server.AspNetCore; using PrivaPub.Models; using System.Net; using static OpenIddict.Server.OpenIddictServerEvents; namespace PrivaPub.Api.Mastodon.Auth { public static class OAuthSetup { public const string LoginScheme = "PrivaPub.OAuth"; public const string OutOfBand = "urn:ietf:wg:oauth:2.0:oob"; public static IServiceCollection PrivaPubOAuth(this IServiceCollection services, IWebHostEnvironment environment) { services.AddSingleton(_ => DB.Default.Database()); services.AddAuthentication() .AddCookie(LoginScheme, options => { options.Cookie.Name = "privapub.oauth"; options.Cookie.Path = "/oauth"; options.Cookie.HttpOnly = true; options.Cookie.SameSite = SameSiteMode.Lax; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.ExpireTimeSpan = TimeSpan.FromMinutes(15); options.SlidingExpiration = false; options.LoginPath = "/oauth/login"; }); services.AddOpenIddict() .AddCore(options => options.UseMongoDb()) .AddServer(options => { options.SetAuthorizationEndpointUris("/oauth/authorize") .SetTokenEndpointUris("/oauth/token") .SetRevocationEndpointUris("/oauth/revoke") .SetConfigurationEndpointUris("/.well-known/openid-configuration", "/.well-known/oauth-authorization-server"); options.AllowAuthorizationCodeFlow().AllowClientCredentialsFlow(); options.RegisterScopes(MastodonScopes.All); options.UseReferenceAccessTokens(); options.SetAccessTokenLifetime(null); options.SetAuthorizationCodeLifetime(TimeSpan.FromMinutes(10)); options.DisableAccessTokenEncryption(); var aspNetCore = options.UseAspNetCore() .EnableAuthorizationEndpointPassthrough() .EnableTokenEndpointPassthrough(); if (!environment.IsProduction()) aspNetCore.DisableTransportSecurityRequirement(); options.AddEventHandler(builder => builder.UseInlineHandler(OutOfBandCode) .SetOrder(OpenIddictServerAspNetCoreHandlers.Authentication.ProcessFormPostResponse.Descriptor.Order - 1_000) .SetType(OpenIddictServerHandlerType.Custom)); options.AddEventHandler(builder => builder.UseInlineHandler(context => { if (context.Response.AccessToken != default) context.Response["created_at"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); return default; })); }) .AddValidation(options => { options.UseLocalServer(); options.UseAspNetCore(); }); services.AddOptions().Configure>((options, app) => { options.Issuer = new Uri(app.CurrentValue.BackendBaseAddress.TrimEnd('/') + "/"); options.SigningCredentials.Add(new SigningCredentials(OidcKey.Load(OidcKey.Signing), SecurityAlgorithms.RsaSha256)); options.EncryptionCredentials.Add(new EncryptingCredentials(OidcKey.Load(OidcKey.Encryption), SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512)); }); return services; } static async ValueTask OutOfBandCode(ApplyAuthorizationResponseContext context) { if (context.RedirectUri != OutOfBand || context.Response.Code == default) return; var http = context.Transaction.GetHttpRequest()?.HttpContext; if (http == default) return; var code = WebUtility.HtmlEncode(context.Response.Code); http.Response.StatusCode = StatusCodes.Status200OK; http.Response.ContentType = "text/html; charset=utf-8"; http.Response.Headers["Content-Security-Policy"] = "default-src 'none'; style-src 'unsafe-inline'"; http.Response.Headers["Cache-Control"] = "no-store"; await http.Response.WriteAsync($$""" Authorization code

Authorization code

Copy this code into the application:

{{code}} """); context.HandleRequest(); } } }