diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index eb66f772b..50fc9d3f3 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -575,7 +575,7 @@ else private async Task CreateToken() { - _token = await UserService.GetTokenAsync(); + _token = await UserService.GetPersonalAccessTokenAsync(); } private void ToggleClientSecret() diff --git a/Oqtane.Client/Services/Interfaces/IUserService.cs b/Oqtane.Client/Services/Interfaces/IUserService.cs index 9a80c7bde..a3f51ebf6 100644 --- a/Oqtane.Client/Services/Interfaces/IUserService.cs +++ b/Oqtane.Client/Services/Interfaces/IUserService.cs @@ -109,5 +109,11 @@ public interface IUserService /// /// Task GetTokenAsync(); + + /// + /// Get personal access token for current user (administrators only) + /// + /// + Task GetPersonalAccessTokenAsync(); } } diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 625d5cd1a..c0b25edc8 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -84,5 +84,10 @@ public async Task GetTokenAsync() { return await GetStringAsync($"{Apiurl}/token"); } + + public async Task GetPersonalAccessTokenAsync() + { + return await GetStringAsync($"{Apiurl}/personalaccesstoken"); + } } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index b741cae08..c7743fb8c 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -522,8 +522,23 @@ public async Task Validate(string password) // GET api//token [HttpGet("token")] - [Authorize(Roles = RoleNames.Admin)] + [Authorize(Roles = RoleNames.Registered)] public string Token() + { + var token = ""; + var sitesettings = HttpContext.GetSiteSettings(); + var secret = sitesettings.GetValue("JwtOptions:Secret", ""); + if (!string.IsNullOrEmpty(secret)) + { + token = _jwtManager.GenerateToken(_tenantManager.GetAlias(), (ClaimsIdentity)User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Audience", "20"))); + } + return token; + } + + // GET api//personalaccesstoken + [HttpGet("personalaccesstoken")] + [Authorize(Roles = RoleNames.Admin)] + public string PersonalAccessToken() { var token = ""; var sitesettings = HttpContext.GetSiteSettings(); diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 7377abfa0..31132a00c 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -72,6 +72,7 @@ internal static IServiceCollection AddOqtaneSingletonServices(this IServiceColle services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index fd28d904d..d8b4cfb07 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -36,6 +36,7 @@ + diff --git a/Oqtane.Server/Security/AutoValidateAntiforgeryTokenAttribute.cs b/Oqtane.Server/Security/AutoValidateAntiforgeryTokenAttribute.cs new file mode 100644 index 000000000..56bf4991a --- /dev/null +++ b/Oqtane.Server/Security/AutoValidateAntiforgeryTokenAttribute.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace Oqtane.Security +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class AutoValidateAntiforgeryTokenAttribute : Attribute, IFilterFactory, IOrderedFilter + { + public int Order { get; set; } = 1000; + + public bool IsReusable => true; + + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + return serviceProvider.GetRequiredService(); + } + } +} diff --git a/Oqtane.Server/Security/AutoValidateAntiforgeryTokenFilter.cs b/Oqtane.Server/Security/AutoValidateAntiforgeryTokenFilter.cs new file mode 100644 index 000000000..9cbf466c9 --- /dev/null +++ b/Oqtane.Server/Security/AutoValidateAntiforgeryTokenFilter.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace Oqtane.Security +{ + public class AutoValidateAntiforgeryTokenFilter : IAsyncAuthorizationFilter, IAntiforgeryPolicy + { + private readonly IAntiforgery _antiforgery; + + public AutoValidateAntiforgeryTokenFilter(IAntiforgery antiforgery) + { + _antiforgery = antiforgery; + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!context.IsEffectivePolicy(this)) + { + return; + } + + if (ShouldValidate(context)) + { + try + { + await _antiforgery.ValidateRequestAsync(context.HttpContext); + } + catch + { + context.Result = new AntiforgeryValidationFailedResult(); + } + } + } + + protected virtual bool ShouldValidate(AuthorizationFilterContext context) + { + // ignore antiforgery validation if a bearer token was provided + if (context.HttpContext.Request.Headers.ContainsKey("Authorization")) + { + return false; + } + + // ignore antiforgery validation for GET, HEAD, TRACE, OPTIONS + var method = context.HttpContext.Request.Method; + if (HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsTrace(method) || HttpMethods.IsOptions(method)) + { + return false; + } + + // everything else requires antiforgery validation (ie. POST, PUT, DELETE) + return true; + } + } +} diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index c1a2aeb2d..afadb1e42 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -128,10 +128,13 @@ public void ConfigureServices(IServiceCollection services) services.AddOqtaneAuthorizationPolicies(); - services.AddMvc() - .AddNewtonsoftJson() - .AddOqtaneApplicationParts() // register any Controllers from custom modules - .ConfigureOqtaneMvc(); // any additional configuration from IStartup classes + services.AddMvc(options => + { + options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); + }) + .AddNewtonsoftJson() + .AddOqtaneApplicationParts() // register any Controllers from custom modules + .ConfigureOqtaneMvc(); // any additional configuration from IStartup classes services.AddSwaggerGen(options => {