In Swagger UI, how can I remove the padlock icon from "anonymous" methods? - jwt

I'm creating an API with .Net Core 2.1 and using JSON Web Token (JWT) for authentication.
I have 2 controllers: AuthenticationController and UserController.
I have decorated AuthenticationController with [AllowAnonymous] and UserController with [Authorize].
Swagger is working correctly: it allows me to hit the endpoints in AuthenticationController (SignUp/SignIn) without requesting authorization, and it does request JWT to hit the endpoints in UserController.
However, in Swagger UI, every endpoint of every controller shows a padlock icon as if all of them required authorization. Everything works correctly and as expected but it just bothers me that the endpoints that don't require authorization still show that padlock icon.
Is there a way to remove the padlock icon from those endpoints?
I believe that something can be done with the OperationFilter but I couldn't find a way.

Absolutly, you need to use an IOperationFilter to remove the padlock icon for the anonymous endpoints.
// AuthResponsesOperationFilter.cs
public class AuthResponsesOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var authAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType<AuthorizeAttribute>();
if (authAttributes.Any())
{
var securityRequirement = new OpenApiSecurityRequirement()
{
{
// Put here you own security scheme, this one is an example
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
};
operation.Security = new List<OpenApiSecurityRequirement> { securityRequirement };
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
}
}
}
// Startup.cs
services.AddSwaggerGen(c =>
{
...
c.OperationFilter<AuthResponsesOperationFilter>();
};
Do not forget to remove any call to AddSecurityRequirement in your Startup.cs, otherwise the padlock icon would still be added to all endpoints.

this solution works for SwashBuckle 5.0.0-rc5 and .Net Core 3.1.1 Web API.
You need to :
implement an IOperationFilter interface,
add c.OperationFilter(); in your Startup.cs file
finally remove any call of AddSecurityRequirement
public class AuthResponsesOperationFilter: IOperationFilter {
public void Apply(OpenApiOperation operation, OperationFilterContext context) {
if (!context.MethodInfo.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute) &&
!context.MethodInfo.DeclaringType.GetCustomAttributes(true).Any(x => x is AllowAnonymousAttribute)) {
operation.Security = new List < OpenApiSecurityRequirement > {
new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme {
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "bearer"
}
}, new string[] {}
}
}
};
}
}
}

Install Package
Swashbuckle.AspNetCore.Filters
And then when you document your swagger you need to add the below line
options.OperationFilter<SecurityRequirementsOperationFilter >();
Here's an example from .NET 6
builder.Services.AddSwaggerGen(options => {
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "API",
Version = "v1",
Description = "API using .NET 6"
});
options.OperationFilter<SecurityRequirementsOperationFilter>();
});

In startup.cs -> services.AddSwaggerGen , you need to add c.OperationFilter<ApplyOAuth2Security>(); and add below method in stratup.cs which will enable lock/authorize icon in Swagger UI for the action methods which are marked as Authorize only.
private class ApplyOAuth2Security : IOperationFilter
{
/// <inheritdoc/>
public void Apply(Operation operation, OperationFilterContext context)
{
var filterDescriptor = context.ApiDescription.ActionDescriptor.FilterDescriptors;
var isAuthorized = filterDescriptor.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter);
var authorizationRequired = context.MethodInfo.CustomAttributes.Any(a => a.AttributeType.Name == "AuthorizeAttribute");
if (isAuthorized && authorizationRequired)
{
operation.Security = new List<IDictionary<string, IEnumerable<string>>>
{
new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", new string[] { "openid" } },
},
};
}
}
}

Related

Create WinUI3/MVVM Most Recently Used (MRU) List in Menu Bar

I would like to create a classic "Recent Files" list in my Windows app menu bar (similar to Visual Studio's menu bar -> File -> Recent Files -> see recent files list)
The MRU list (List < string > myMRUList...) is known and is not in focus of this question. The problem is how to display and bind/interact with the list according to the MVVM rules.
Microsoft.Toolkit.Uwp.UI.Controls's Menu class will be removed in a future release and they recommend to use MenuBar control from the WinUI. I haven't found any examples, that use WinUI's MenuBar to create a "Recent Files" list.
I'm using Template Studio to create a WinUI 3 app. In the ShellPage.xaml I added
<MenuFlyoutSubItem x:Name="mruFlyout" Text="Recent Files"></MenuFlyoutSubItem>
and in ShellPage.xaml.c
private void Button_Click(object sender, RoutedEventArgs e)
{
mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test1_" + DateTime.Now.ToString("MMMM dd") } );
mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test2_" + DateTime.Now.ToString("MMMM dd") } );
mruFlyout.Items.Insert(mruFlyout.Items.Count, new MenuFlyoutItem(){ Text = "C:\\Test3_" + DateTime.Now.ToString("MMMM dd") } );
}
knowing this is not MVVM, but even this approach does not work properly, because the dynamically generated MenuFlyoutItem can be updated only once by Button_Click() event.
Could anybody give me an example, how to create the "Recent Files" functionality, but any help would be great! Thanks
Unfortunately, it seems that there is no better solution than handling this in code behind since the Items collection is readonly and also doesn't response to changes in the UI Layout.
In addition to that, note that because of https://github.com/microsoft/microsoft-ui-xaml/issues/7797, updating the Items collection does not get reflected until the Flyout has been closed and reopened.
So assuming your ViewModel has an ObservableCollection, I would probably do this:
// 1. Register collection changed
MyViewModel.RecentFiles.CollectionChanged += RecentFilesChanged;
// 2. Handle collection change
private void RecentFilesChanged(object sender, NotifyCollectionChangedEventArgs args)
{
// 3. Create new UI collection
var flyoutItems = list.Select(entry =>
new MenuFlyoutItem()
{
Text = entry.Name
}
);
// 4. Updating your MenuFlyoutItem
mruFlyout.Items.Clear();
flyoutItems.ForEach(entry => mruFlyout.Items.Add(entry));
}
Based on chingucoding's answer I got to the "recent files list" binding working.
For completeness I post the detailed code snippets here (keep in mind, that I'm not an expert):
Again using Template Studio to create a WinUI 3 app.
ShellViewModel.cs
// constructor
public ShellViewModel(INavigationService navigationService, ILocalSettingsService localSettingsService)
{
...
MRUUpdateItems();
}
ShellViewModel_RecentFiles.cs ( <-- partial class )
using System.Collections.ObjectModel;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Windows.Storage;
using Windows.Storage.AccessCache;
using Windows.Storage.Pickers;
namespace App_MostRecentUsedTest.ViewModels;
public partial class ShellViewModel : ObservableRecipient
{
public ObservableCollection<MRUItem> MRUItems{ get; set;} = new();
// update ObservableCollection<MRUItem>MRUItems from MostRecentlyUsedList
public void MRUUpdateItems()
{
var mruTokenList = StorageApplicationPermissions.MostRecentlyUsedList.Entries.Select(entry => entry.Token).ToList();
var mruMetadataList = StorageApplicationPermissions.MostRecentlyUsedList.Entries.Select(entry => entry.Metadata).ToList(); // contains path as string
MRUItems.Clear(); var i = 0;
foreach (var path in mruMetadataList)
{
MRUItems.Add(new MRUItem() { Path = path, Token = mruTokenList[i++] });
}
}
// called if user selects a recent used file from menu bar list
[RelayCommand]
protected async Task MRULoadFileClicked(int? fileId)
{
if (fileId is not null)
{
var mruItem = MRUItems[(int)fileId];
FileInfo fInfo = new FileInfo(mruItem.Path ?? "");
if (fInfo.Exists)
{
StorageFile? file = await Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.GetFileAsync(mruItem.Token);
if (file is not null)
{
Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.Add(file, file.Path); // store file.Path into Metadata
MRUUpdateItems();
// LOAD_FILE(file);
}
}
else
{
}
}
await Task.CompletedTask;
}
[RelayCommand]
protected async Task MenuLoadFileClicked()
{
StorageFile? file = await GetFilePathAsync();
if (file is not null)
{
Windows.Storage.AccessCache.StorageApplicationPermissions.MostRecentlyUsedList.Add(file, file.Path); // store file.Path into Metadata
MRUUpdateItems();
// LOAD_FILE(file);
}
await Task.CompletedTask;
}
// get file path with filePicker
private async Task<StorageFile?> GetFilePathAsync()
{
FileOpenPicker filePicker = new();
filePicker.FileTypeFilter.Add(".txt");
IntPtr hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(filePicker, hwnd);
return await filePicker.PickSingleFileAsync();
}
public class MRUItem : INotifyPropertyChanged
{
private string? path;
private string? token;
public string? Path
{
get => path;
set
{
path = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(path));
}
}
public string? Token
{
get => token;
set => token = value;
}
public event PropertyChangedEventHandler? PropertyChanged;
}
}
ShellPage.xaml
<MenuBar>
<MenuBarItem x:Name="ShellMenuBarItem_File">
<MenuFlyoutItem x:Uid="ShellMenuItem_File_Load" Command="{x:Bind ViewModel.MenuLoadFileClickedCommand}" />
<MenuFlyoutSubItem x:Name="MRUFlyout" Text="Recent Files..." />
</MenuBarItem>
</MenuBar>
ShellPage.xaml.cs
// constructor
public ShellPage(ShellViewModel viewModel)
{
...
// MRU initialziation
// assign RecentFilesChanged() to CollectionChanged-event
ViewModel.MRUItems.CollectionChanged += RecentFilesChanged;
// Add (and RemoveAt) trigger RecentFilesChanged-event to update MenuFlyoutItems
ViewModel.MRUItems.Add(new MRUItem() { Path = "", Token = ""});
ViewModel.MRUItems.RemoveAt(ViewModel.MRUItems.Count - 1);
}
// MRU Handle collection change
private void RecentFilesChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// project each MRUItems list element into a new UI MenuFlyoutItem flyoutItems list
var i = 0;
var flyoutItems = ViewModel.MRUItems.Select(entry =>
new MenuFlyoutItem()
{
Text = " " + i.ToString() + " " + FilenameHelper.EllipsisString(entry.Path, 65),
Command = ViewModel.MRULoadFileClickedCommand,
CommandParameter = i++
}
);
//// If you want to update the list while it is shown,
//// you will need to create a new FlyoutItem because of
//// https://github.com/microsoft/microsoft-ui-xaml/issues/7797
// Create a new flyout and populate it
var newFlyout = new MenuFlyoutSubItem();
newFlyout.Text = MRUFlyout.Text; // Text="Recent Files...";
// Updating your MenuFlyoutItem
flyoutItems.ToList().ForEach(item => newFlyout.Items.Add(item));
// Get index of old sub item and remove it
var oldIndex = ShellMenuBarItem_File.Items.IndexOf(MRUFlyout);
ShellMenuBarItem_File.Items.Remove(MRUFlyout);
// Insert the new flyout at the correct position
ShellMenuBarItem_File.Items.Insert(oldIndex, newFlyout);
// Assign newFlyout to "old"-MRUFlyout
MRUFlyout = newFlyout;
}

EF Core 5.x + OData 7.5.6 - $Top doesn't insert TOP inside of EF Query

I'm thinking I missed something very simply but here is what I'm trying to todo. I have a .NET Core 5 project with EF Core 5 + OData 7.5.6. Everything appears to be working except for INSERT a TOP command in the generated SQL Query. Here is my controller. Very simple.
[EnableQuery]
[ApiController]
[Route("odata/[controller]")]
public class ConferenceHistoryController : ControllerBase
{
private readonly cdr_database_2Context _db;
public ConferenceHistoryController(cdr_database_2Context db)
{
_db = db;
}
[HttpGet]
public IEnumerable<_000701CallDataRecord> GetConferenceList()
{
// Return Full List
var query = _db._000701CallDataRecords;
var qs = query.ToQueryString();
return query.ToList();
}
}
When I send in my request to:
https://localhost:44355/odata/ConferenceHistory/?$select=RecordId,version&$top=5
The resulting SQL query is:
SELECT [0].[endDateTime], [0].[id], [0].[organizer], [0].[ParticipantCount], [0].[participants], [0].[PoorCall], [0].[type], [0].[version]
FROM [000701_CallDataRecord] AS [0]
As you can see, it's missing both the TOP and the SELECT commands. I have the following in my Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Add CORS to Project
services.AddCors(options =>
{
options.AddDefaultPolicy(builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
// Add OData
services.AddOData();
services.AddControllers();
services.AddDbContext<cdr_database_2Context>(options =>
options.UseSqlServer(Configuration.GetConnectionString("CDRConnection"))
);
// Add Swagger Support
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "ODataAPI", Version = "v1" });
});
SetOutputFormatters(services);
}
And
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Use HTTPS & Routing
app.UseHttpsRedirection();
app.UseRouting();
// Auth???
app.UseAuthorization();
// Swagger Configure
app.UseSwagger();
app.UseSwaggerUI((config) =>
{
config.SwaggerEndpoint("/swagger/v1/swagger.json", "Swagger Odata Demo Api");
});
// Setup Endpoint for EDM
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.EnableDependencyInjection();
endpoints.Select().Expand().Filter().OrderBy().MaxTop(50).Count();
endpoints.MapODataRoute("odata", "odata", GetEdmModel());
});
}
And
private IEdmModel GetEdmModel()
{
// Add OData - EDM Definitions Below
var odataBuilder = new ODataConventionModelBuilder();
odataBuilder.EntitySet<WeatherForecast>("WeatherForecast");
odataBuilder.EntitySet<_000701CallDataRecord>("ConferenceHistory");
return odataBuilder.GetEdmModel();
}
Just looking for some direction on where I could have gone wrong. Everything else seems to be working really well.
Your controller needs to derive from ODataController not ControllerBase
You need to decorate GetConferenceList() with [Queryable]
OK, I figured out where the issue was in my above. It was with using the .ToList and the IEumerable. So if I changed this:
[HttpGet]
public IEnumerable<_000701CallDataRecord> GetConferenceList()
{
// Return Full List
var query = _db._000701CallDataRecords;
var qs = query.ToQueryString();
return query.ToList();
}
To:
[HttpGet]
public IEnumerable<_000701CallDataRecord> GetConferenceList()
{
// Return Full List
var query = _db._000701CallDataRecords;
var qs = query.ToQueryString();
return query;
}
It works. It's also worth noting that the .ToQueryString() doesn't show the OData/EF insertion of the TOP command. But if I enable .EnableSensitiveDataLogging() in the Startup.cs file, then I clearly see it being inserted into it.
Apparently, calling .ToList() will cause it to process and ignore any of the fancy OData commands. That was the only change and all was good then.

Return a custom response when using the Authorize Attribute on a controller

I have just implemented the Bearer token and I have added the Authorize attribute to my controller class and that works fine. It looks like this:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
What I would like to do is to create a more complex response from the server when it fails, rather then the standard 401.
I tried filters but they are not invoked at all.
Any ideas how to do this?
Have a custom scheme, custom authorization handler and poof!
Notice that I injected the Handler in ConfigureServices:
services.AddAuthentication(options =>
{
options.DefaultScheme = ApiKeyAuthenticationOptions.DefaultScheme;
options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme;
})
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
ApiKeyAuthenticationOptions.DefaultScheme, o => { });
ApiKeyAuthenticationOptions
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "API Key";
public string Scheme => DefaultScheme;
public string AuthenticationType = DefaultScheme;
public const string HeaderKey = "X-Api-Key";
}
ApiKeyAuthenticationHandler
/// <summary>
/// An Auth handler to handle authentication for a .NET Core project via Api keys.
///
/// This helps to resolve dependency issues when utilises a non-conventional method.
/// https://stackoverflow.com/questions/47324129/no-authenticationscheme-was-specified-and-there-was-no-defaultchallengescheme-f
/// </summary>
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private readonly IServiceProvider _serviceProvider;
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options, ILoggerFactory logger,
UrlEncoder encoder, ISystemClock clock, IServiceProvider serviceProvider)
: base (options, logger, encoder, clock)
{
_serviceProvider = serviceProvider;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var token = Request.Headers[ApiKeyAuthenticationOptions.HeaderKey];
if (string.IsNullOrEmpty(token)) {
return Task.FromResult (AuthenticateResult.Fail ("Token is null"));
}
var customRedisEvent = _serviceProvider.GetRequiredService<ICustomRedisEvent>();
var isValidToken = customRedisEvent.Exists(token, RedisDatabases.ApiKeyUser);
if (!isValidToken) {
return Task.FromResult (AuthenticateResult.Fail ($"Invalid token {token}."));
}
var claims = new [] { new Claim ("token", token) };
var identity = new ClaimsIdentity (claims, nameof (ApiKeyAuthenticationHandler));
var ticket = new AuthenticationTicket (new ClaimsPrincipal (identity), Scheme.Name);
return Task.FromResult (AuthenticateResult.Success (ticket));
}
}
Focus on the handler class. Apart from the sample code I've provided, simply utilise the base class properties like Response to set your custom http status code or whatever you may need!
Here's the derived class if you need it.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.authenticationhandler-1?view=aspnetcore-3.1

How to override route in a plugin nopcommerce

I've a route like Admin/Vendor in my MVC application . Without changing this route I need to point this same route to another method say CustomAdmin/CustomVendor.
I tried attribute routing but no luck . Is there any way to do this. My current code is given below
Original Method:
public class AdminController
{
public ActionResult Vendor()
{
return View();
}
}
Custom Method:
public class CustomAdminController
{
[Route("Admin/Vendor")]
public ActionResult CustomVendor()
{
return View();
}
}
As you're developing a plugin. You have to add your custom route to the RouteProvider.
In default nopCommerce AdminController and Vendor doesn't exists, so I assume that you're trying to override vendor list method of admin.
Which looks like:
public partial class RouteProvider : IRouteProvider
{
public void RegisterRoutes(RouteCollection routes)
{
var route = routes.MapRoute("Plugin.GroupName.PluginName.CustomVendor",
"Admin/Vendor/List",
new { controller = "CustomAdminController", action = "CustomVendor", orderIds = UrlParameter.Optional, area = "Admin" },
new[] { "Nop.Plugin.GroupName.PluginName.Controllers" });
route.DataTokens.Add("area", "admin");
routes.Remove(route);
routes.Insert(0, route);
}
public int Priority
{
get
{
return 100; // route priority
}
}
}
Side Note: GroupName and PluginName should be your plugin group name and plugin name.
Hope this helps !
On your plugin which class implements the interface IRouteProvider, you can easily override the route there.
Likewise I have a class named RouteProvider in my plugin, So I have Implemented the abstract function RegisterRoutes and simply it can be overrided by
routes.MapRoute("Plugin.Promotion.Combo.SaveGeneralSettings",
"Admin/Vendor",
new { controller = "CustomAdmin", action = "CustomVendor", },
new[] { "Nop.Plugin.Promotion.Combo.Controllers" }
);
Here Plugin.Promotion.Combo must be replaced by your plugin directory.And using SaveGeneralSettings or any things you want to use that will be your route url

Request for declined permissions. Unity for Facebook

Been trying to request for declined permission with FB.login("Scope") but it redirects me to the login page again. After logging in, it prompts me with a "You have already authorised APPNAME." FB.login doesn't support "auth_type".
Does anyone have any solutions to this problem?
Thanks~
May the force be with you
Not sure for which platform you've been trying to re-ask declined permissions, probably for Canvas. I can only suggest solution for Android.
First, we need to hack provided AndroidFacebook and FB classes. To prevent overriding of our changes with plugin update in the future, we almost don't touch third party files, just make classes partial:
// AndroidFacebook.cs
namespace Facebook
{
sealed partial class AndroidFacebook : AbstractFacebook, IFacebook
// FB.cs
public sealed partial class FB : ScriptableObject
Now we can extend existing classes with a couple of methods:
using System.Collections.Generic;
namespace Facebook
{
partial class AndroidFacebook
{
public void LoginWithParameters(IDictionary<string, object> parameters, FacebookDelegate callback = null)
{
if (parameters == null)
parameters = new Dictionary<string, object>();
var paramJson = MiniJSON.Json.Serialize(parameters);
AddAuthDelegate(callback);
CallFB("Login", paramJson);
}
}
}
partial class FB
{
public static bool TryLoginWithParametersOnAndroid(IDictionary<string, object> parameters, Facebook.FacebookDelegate callback = null)
{
var androidFacebook = FacebookImpl as Facebook.AndroidFacebook;
if (androidFacebook == null)
return false;
androidFacebook.LoginWithParameters(parameters, callback);
return true;
}
}
Here is how we can pass scope and auth_type:
var parameters = new Dictionary<string, object>
{
{ "scope", "email" },
{ "auth_type", "rerequest" },
};
var apiCallSucceeded = FB.TryLoginWithParametersOnAndroid(parameters, result =>
{
...
});