diff --git a/contrib/bruno/blog post.yml b/contrib/bruno/blog post.yml new file mode 100644 index 00000000..94e2745f --- /dev/null +++ b/contrib/bruno/blog post.yml @@ -0,0 +1,22 @@ +info: + name: blog post + type: http + seq: 2 + +http: + method: POST + url: "{{Blogs}}/api/v1/blog" + body: + type: json + data: |- + { + "Title": "lkijlk", + "Article": "test" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/contrib/bruno/opencollection.yml b/contrib/bruno/opencollection.yml index 701cace5..374cd0e1 100644 --- a/contrib/bruno/opencollection.yml +++ b/contrib/bruno/opencollection.yml @@ -25,7 +25,7 @@ request: credentials: clientId: postit placement: basic_auth_header - scope: openid blogs + scope: openid blogs profile pkce: {} tokenConfig: id: credentials diff --git a/src/Yavsc.Abstract/Constants.cs b/src/Yavsc.Abstract/Constants.cs index d303b629..af78ab0b 100644 --- a/src/Yavsc.Abstract/Constants.cs +++ b/src/Yavsc.Abstract/Constants.cs @@ -57,6 +57,8 @@ namespace Yavsc public const string StreamingPath = "/api/stream/put"; + public static string NameClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/name"; + public static string RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; } } diff --git a/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs b/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs index c2700e93..04bc8e33 100644 --- a/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs +++ b/src/Yavsc.Abstract/Identity/UserDisplayHelpers.cs @@ -28,7 +28,7 @@ namespace Yavsc.Abstract.Identity /// public static string AvatarSrc(IApplicationUser? user) { - if (string.IsNullOrWhiteSpace(user?.UserName)) + if (user==null || string.IsNullOrWhiteSpace(user?.UserName)) return YavscConstants.DefaultAvatar; return $"{YavscConstants.AvatarsPath}/{user!.UserName}.s.png"; } diff --git a/src/Yavsc.Blogs/Controllers/BlogApiController.cs b/src/Yavsc.Blogs/Controllers/BlogApiController.cs index 31afa163..6a41b4ee 100644 --- a/src/Yavsc.Blogs/Controllers/BlogApiController.cs +++ b/src/Yavsc.Blogs/Controllers/BlogApiController.cs @@ -83,7 +83,7 @@ namespace Yavsc.Blogs.Controllers return new StatusCodeResult(StatusCodes.Status204NoContent); } - // POST: api/BlogApi + // POST: api/v1/blog [HttpPost] public IActionResult PostBlog([FromBody] Models.Blog.BlogPost blog) { diff --git a/src/Yavsc.Blogs/Program.cs b/src/Yavsc.Blogs/Program.cs index ae4e209c..d2e2e87a 100644 --- a/src/Yavsc.Blogs/Program.cs +++ b/src/Yavsc.Blogs/Program.cs @@ -37,7 +37,7 @@ internal class Program .AddYavscCors(builder.Configuration) .AddControllers(); String authority = builder.Configuration.GetValue("Site:Authority"); - String audience = builder.Configuration.GetValue("Site:Audience"); + if (string.IsNullOrEmpty(authority)) { throw new Exception("Site:Authority is not configured in appsettings.json"); @@ -100,7 +100,7 @@ internal class Program .UseCors("default") ; app.Services.GetRequiredService().CreateLogger("Program") - .LogInformation($"Yavsc.Blogs started, Authority is '{authority}', Audience is '{audience}'"); + .LogInformation($"Yavsc.Blogs started, Authority is '{authority}''"); app.MapControllers(); app.MapIdentityApi().RequireAuthorization("BlogScope") .WithHttpLogging(Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All) diff --git a/src/Yavsc.Blogs/appsettings-blogs.json b/src/Yavsc.Blogs/appsettings-blogs.json index a33b8102..a5453186 100644 --- a/src/Yavsc.Blogs/appsettings-blogs.json +++ b/src/Yavsc.Blogs/appsettings-blogs.json @@ -4,6 +4,7 @@ "Description": "A collection of blogs about software development, technology, and programming.", "Keywords": "software development, technology, programming, coding, blogs", "Authority": "https://localhost:5001", + "Audience": ["blogs"], "CorsAllowedOrigins": [ "https://localhost:5005" ] diff --git a/src/Yavsc.Org/Controllers/Communicating/BlogspotController.cs b/src/Yavsc.Org/Controllers/Communicating/BlogspotController.cs index a357458a..f59b8b82 100644 --- a/src/Yavsc.Org/Controllers/Communicating/BlogspotController.cs +++ b/src/Yavsc.Org/Controllers/Communicating/BlogspotController.cs @@ -10,7 +10,7 @@ using Yavsc.Server.Helpers; // For more information on enabling Web API for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 -namespace Yavsc.Controllers +namespace Yavsc.Org.Controllers { public class BlogSpotController : Controller { @@ -28,7 +28,7 @@ namespace Yavsc.Controllers BlogSpotService blogSpotService) { _context = context; - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); _authorizationService = authorizationService; _localisationOptions = localisationOptions.Value; this.blogSpotService = blogSpotService; @@ -76,7 +76,7 @@ namespace Yavsc.Controllers return View(blog); } - catch (AuthorizationFailureException ex) + catch (AuthorizationFailureException) { return Challenge(); } diff --git a/src/Yavsc.Org/Extensions/HostingExtensions.cs b/src/Yavsc.Org/Extensions/HostingExtensions.cs index 6fc5b9c4..a494a05b 100644 --- a/src/Yavsc.Org/Extensions/HostingExtensions.cs +++ b/src/Yavsc.Org/Extensions/HostingExtensions.cs @@ -157,8 +157,7 @@ public static class HostingExtensions services.AddAuthentication("Bearer") - .AddYavscJwtBearer(builder.Configuration, - configure: o => o.Audience = builder.Configuration.GetSection("Site")["ExternalUrl"]); + .AddYavscJwtBearer(builder.Configuration); services.AddTransient>(); services.AddTransient, RoleStore>(); diff --git a/src/Yavsc.Org/Services/BlogSpotService.cs b/src/Yavsc.Org/Services/BlogSpotService.cs index 8377984f..3700e04f 100644 --- a/src/Yavsc.Org/Services/BlogSpotService.cs +++ b/src/Yavsc.Org/Services/BlogSpotService.cs @@ -1,26 +1,24 @@ using System.Diagnostics; using System.Security.Claims; using Microsoft.AspNetCore.Authorization; -using Microsoft.DotNet.Scaffolding.Shared; using Microsoft.EntityFrameworkCore; using Yavsc; -using Yavsc.Helpers; using Yavsc.Models; using Yavsc.Models.Blog; using Yavsc.Server.Exceptions; using Yavsc.Server.Helpers; using Yavsc.Services; using Yavsc.ViewModels.Auth; -using Yavsc.Abstract.Helpers; -using Microsoft.AspNetCore.Http; -public class BlogSpotService + +[Obsolete] +public class OldBlogSpotService { private readonly ApplicationDbContext _context; private readonly IAuthorizationService _authorizationService; private readonly IFileSystemAuthManager fileSystemAuthManager; - public BlogSpotService(ApplicationDbContext context, + public OldBlogSpotService(ApplicationDbContext context, IAuthorizationService authorizationService, IFileSystemAuthManager fileSystemAuthManager) { @@ -93,13 +91,13 @@ public class BlogSpotService public async Task GetPostForEdition(ClaimsPrincipal user, long blogPostId) { var blog = await _context.BlogSpot.Include(x => x.Author).Include(x => x.ACL).SingleAsync(m => m.Id == blogPostId); - var auth = await _authorizationService.AuthorizeAsync(user, blog, new EditPermission()); + var auth = await _authorizationService.AuthorizeAsync(user, blog, new EditPermission()); if (!auth.Succeeded) { throw new AuthorizationFailureException(auth); - } + } var pub = await _context.blogSpotPublications.AnyAsync(x => x.BlogpostId == blog.Id); - + return new BlogPostEditViewModel(blog, pub); } diff --git a/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml b/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml index 2e4ef1bb..f0a961c9 100644 --- a/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml +++ b/src/Yavsc.Org/Views/Shared/DisplayTemplates/ApplicationUser.cshtml @@ -1,14 +1,19 @@ @using Yavsc.Abstract.Identity @model ApplicationUser -@{ - // Le helper défend contre Model null et contre UserName vide - // ou whitespace. Sans cette garde, Razor lève - // NullReferenceException ici, ce qui propage un 500 et - // masque aussi la page d'erreur. - var avuri = UserDisplayHelpers.AvatarSrc(Model); -} + diff --git a/src/Yavsc.Server/Helpers/ServiceExtensions.cs b/src/Yavsc.Server/Helpers/ServiceExtensions.cs index 3abc723b..f074b9e7 100644 --- a/src/Yavsc.Server/Helpers/ServiceExtensions.cs +++ b/src/Yavsc.Server/Helpers/ServiceExtensions.cs @@ -74,48 +74,59 @@ public static class ServiceExtensions public static AuthenticationBuilder AddYavscJwtBearer( this AuthenticationBuilder builder, IConfiguration configuration, - Action? configure = null, string schemeName = "Bearer") { var authority = configuration.GetSection("Site")["Authority"] ?? throw new InvalidOperationException( "Site:Authority is required to configure Yavsc JWT Bearer authentication."); - return builder.AddJwtBearer(schemeName, options => - { - options.IncludeErrorDetails = true; - options.Authority = authority; - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateAudience = false, - RoleClaimType = YavscConstants.RoleClaimType - }; - options.MapInboundClaims = true; - // Dev: every Yavsc resource service (Yavsc.Api, Yavsc.Blogs, - // Yavsc.Org itself) validates JWTs against the OP that runs - // on https://localhost:5001 with a self-signed dev cert. - // The default .NET HttpClient rejects self-signed certs, so - // JwtBearer's backchannel silently fails to fetch the OIDC - // discovery + JWKS. With an empty ValidIssuer, every token - // is rejected with IDX10204 ("ValidIssuer is null or - // whitespace"). Telling the backchannel to skip TLS - // validation unblocks discovery in dev. Production uses a - // real CA-signed cert and the default validation path; the - // override is gated on HostingEnvironment == Development - // and only fires when the consumer opt-in via the - // 'Yavsc:Dev:TlsInsecure' configuration flag (default - // false), so a misconfigured production environment cannot - // silently downgrade TLS. - if (configuration.GetValue("ASPNETCORE_ENVIRONMENT") == "Development") + string[] audiences = configuration.GetSection("Site").GetSection("Audience").Get() ?? Array.Empty(); + AuthenticationBuilder result = builder; + foreach (var audience in audiences) + { + result = builder.AddJwtBearer(schemeName, options => { - options.BackchannelHttpHandler = new HttpClientHandler + options.IncludeErrorDetails = true; + options.Authority = authority; + options.TokenValidationParameters = new TokenValidationParameters { - ServerCertificateCustomValidationCallback = - (_, _, _, _) => true + ValidateAudience = true, + ValidAudience = audience, + RoleClaimType = YavscConstants.RoleClaimType, + NameClaimType = YavscConstants.NameClaimType, }; - } - configure?.Invoke(options); - }); + options.MapInboundClaims = true; + options.ClaimsIssuer = authority; + options.Audience = audience; + + // Dev: every Yavsc resource service (Yavsc.Api, Yavsc.Blogs, + // Yavsc.Org itself) validates JWTs against the OP that runs + // on https://localhost:5001 with a self-signed dev cert. + // The default .NET HttpClient rejects self-signed certs, so + // JwtBearer's backchannel silently fails to fetch the OIDC + // discovery + JWKS. With an empty ValidIssuer, every token + // is rejected with IDX10204 ("ValidIssuer is null or + // whitespace"). Telling the backchannel to skip TLS + // validation unblocks discovery in dev. Production uses a + // real CA-signed cert and the default validation path; the + // override is gated on HostingEnvironment == Development + // and only fires when the consumer opt-in via the + // 'Yavsc:Dev:TlsInsecure' configuration flag (default + // false), so a misconfigured production environment cannot + // silently downgrade TLS. + if (configuration.GetValue("ASPNETCORE_ENVIRONMENT") == "Development") + { + options.BackchannelHttpHandler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = + (_, _, _, _) => true + }; + } + }); + } + + return result; + } } diff --git a/src/Yavsc.Server/Helpers/UserHelpers.cs b/src/Yavsc.Server/Helpers/UserHelpers.cs index 63716963..105c1beb 100644 --- a/src/Yavsc.Server/Helpers/UserHelpers.cs +++ b/src/Yavsc.Server/Helpers/UserHelpers.cs @@ -29,7 +29,7 @@ namespace Yavsc.Server.Helpers (x.ACL.Count == 0 || x.ACL.Any(a => readerCirclesMemberships.Contains(a.CircleId)))); } } - + public static string GetUserId(this ClaimsPrincipal user) { return user.FindFirstValue("sub"); diff --git a/src/cli/Settings/ConnectionSettings.cs b/src/cli/Settings/ConnectionSettings.cs index 903b14ef..b7a1110a 100644 --- a/src/cli/Settings/ConnectionSettings.cs +++ b/src/cli/Settings/ConnectionSettings.cs @@ -10,7 +10,7 @@ namespace cli public string ClientId { get; set; } public string ClientSecret { get; set; } public string Authority { get; set; } - public string Audience { get; set; } + public string[] Audience { get; set; } public string SiteAccessSheme { get; set; } = "http"; public int Port { get; set; } public string Scope { get; set; } = "profile"; @@ -21,14 +21,14 @@ namespace cli return Port==0 ? $"{SiteAccessSheme}://{Authority}/authorize" : $"{SiteAccessSheme}://{Authority}:{Port}/authorize" ; } } - + [NotMapped] [JsonIgnore] public string RedirectUrl {get { return Port==0 ? $"{SiteAccessSheme}://{Authority}/oauth/success" : $"{SiteAccessSheme}://{Authority}:{Port}/oauth/success" ; } } - + [NotMapped] [JsonIgnore] public string AccessTokenUrl { get { diff --git a/src/cli/connectionsettings.Development2.json b/src/cli/connectionsettings.Development2.json deleted file mode 100644 index c78dd182..00000000 --- a/src/cli/connectionsettings.Development2.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "UserConnection": { - "UserName": "Paul", - "AccessToken": "IvvrnJye2b7CSFp2Hj3mEEK1-7EMbLLGJATQHAwOlRt5sIcm9aMH85kJMAxDb4yIzL76G7maoIlaWuSyhhS3qfcIB04WcVnCVdmw22ncMs8rk_L0yGuLdNBnF3HuhZXpjD-AjpLJ1TdE0q3c-yakkP_EWTjM9I00a1gGop4bgE_-LAa2uoLeaCj0dbe95OSvVKJWJ4MU0fjlvqAaUx-EC4HYicUMEQvJviR0FyyMKPYYyx4jyu_bgYbhmKX9LoE5VeEDEMsyXZE0fxqdfnflICduUqrprCvQfLoG4DZ8o7ivjoak3-xiP7CFaF1vou89mO1c9BcJYUjcZXPgNYC7K9QHwFC5sPDkOHdijUo9xfieRaaqge5tEHlZAs3H_u-bGWft7xjxHOVUPSRzzYygR27alVSUIt6rt28FoLsEGYQaVK5QNzIbOE0RVU40vQb5a6JzcTcjTtRxCMn3FneKZtilEHb2TooUy4YbmhbnDIKsa0ZMMBEshyEel8oCWz9SITjm24FbdCmUvSKOlkHhIcnSM8oez9qhDlzzcsKzahbNeO-EBINW-6qBlKISQbFmfy-zO134JEkcpfA9NybGcYPZWLWu1IrF9pDzO66X2Rh-H45pHQWRWFodHcoQh0p9ZBNwo5k_EkU731uwQZF5zgGyZnUU2VpxdjorzrpexZXpStzYnYRbGGK7kGAUZH9v16zL-nB4t55Mu12iK9jsUIS7cmqOSTj4mJWFd03dwe4xkNrLgcGSGPebmsfSWYaIk7qAmWMsPKKv_vnOpi-1sZuOgdVyH-RmuyW83IPCUiEAf63a40PuqpVxFiMmbcRqSKvh5TedjWUiEZvBvz786-atYF_b0dxCj8fDQbyWiMj-4Lj7FV4gUJFvrDX4DsRns6wL6Cs3urL4PmKHLEhRma-i589pEKmzinO7l4S2atB6uqMS_xNU7Y0um_GQ51oPk8PxxCggI2aLR4PJbuBGSJBoTDW1OV9HNa_BybrKamN8cuc8IT3LBJiHH_ImHyW1n94C2oFcaUJ1dVVt6JDJqt4EVvFiwk15SlRJMXSLWQWxPKeOTCGb-So2Gcy_DwGu8_AlVL4mOdQI0GiGRkpaWNIF3N6ek-B_7Lz1zxKX7BCx1ISurAzG7jK1FDydovJ2LOHO6QbqVH1JKRdRDCLOKZl3TpcAyOQHDo5rAl84pwf5UROMb5SfVDJtdekb7SRR", - "TokenType": "bearer", - "ExpiresIn": "1199", - "RefreshToken": "CfDJ8LSG6cXzxTtFuur3SY2RDINpvQVezgrIru8TEIfKUpNmUXe5lA4tCQ_nlkklgarH5UocJ6b95NLRon0uvVy5mE-7EY3ld_-bjLgjDnV9oPxY7Xdgb94_5HdDWcyG_Zaw67T0G_t21bvNEgtw0zewbdisPpjKopfH7avQWxlRrMbN9Wboj2ZXYVCbmu2EkeyG5665-B16UEQFF7dVT6qU4qnSldIfhx5ex8Ii5aGg0pDAEnRUNla_iObls2yAf18bmTAfF-34kmClqqLCr7z4Cy65DwQ8EHNA9oIEvgCEuj04IG5wC79vycZrw0NLQWAz7fTahRreD_WsmbIKdUqkvlDjV1W3Dq7KUCi4bAk7oAJuqdl3qZg4SohmQ31IKD08aKSOPa4_jtJ-eKOlT6fdDhpoaP3DyKS09OkvAwbMA86937IrCCFZsGBhHAm-yszHXjVjPCr7gAJYaBjneAUAoseEYK0GaOVGXfhxQcURXtM9TyH3pgHjccOzq300Djv-BpfQZcSZnQf59kulL_gt--451r8UIw3wxQ6BD5fGVF_MrilUluLyhoqoGZJbiHFyL-miKjW7feGFuogVyee0nIO1Me-7T-aTQnn-LivP9avyHab4eGwg1nfUbEspA21vmz2UXkUvKvT-JRGmSxlYineeJURQMcGGbcvr9GbFy1A_vbImUSw_Tx0u7_7jwxkDwlTt5WvGVsFFHsj9vgtNSnWHRzbGhbOSBocg0eWylgmYy3yZtdajRx6xQluRUewP_K66GsmA6xVmekUo0_ZzUPj61VQzTMR-knYE-pCd6V_qGS4qtUNeQ5nPLWT6hRPvnd3kK7CS22ijEErm3LaOY2DogGYIoBLvmdande226KW48hUgQPxsmOLoUl1tXMYsiRqHKAnWlzg4-e_BfRe3YsMUwlNPcqYf0hQ-mE-j4fVo79XJFFSWF8WxYKed-imDYeK9b6bQPP3ZPaqiMy7_d9Fxnb3i2Oo4XQPmrTpDkHbMovslBfBoJW4RSpXo3cn0QqcCDG2KfFS8HIYr68jXnyx7Ws3MeswdyEFSqx-XWmZa" - }, - "Connection": { - "ClientId": "53f4d5da-93a9-4584-82f9-b8fdf243b002", - "ClientSecret": "blouh", - "Authority": "localhost", - "Audience": "localhost", - "SiteAccessSheme": "http", - "Port": 5000, - "Scope": "profile" - } -}