Swagger UI - authentication only for some endpoints - swagger

I am using Swagger in a .NET COre API project.
Is there a way to apply JWT Authentication in Swagger UI only for some endpoints?
I put [Authorize] Attribute only on a few calls (also have tried putting [AllowAnonymous] on the calls that don't need authentication), but when I open the Swagger UI page, the lock symbol is on all the endpoints.

You'll have to create an IOperationFilter to only add the OpenApiSecurityScheme to certain endpoints. How this can be done is described in this blog post (adjusted for .NET Core 3.1, from a comment in the same blog post).
In my case, all endpoints defaults to [Authorize] if not [AllowAnonymous] is explicitly added (also described in the linked blog post). I then create the following implementation of IOperationFilter:
public class SecurityRequirementsOperationFilter : 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) ?? false))
{
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme {
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "bearer"
}
}, new string[] { }
}
}
};
}
}
}
You'll have to tweak the if statement if you don't default all endpoints to [Authorize].
Finally, where I call services.AddSwaggerGen(options => { ... } (usually in Startup.cs) I have the following line:
options.OperationFilter<SecurityRequirementsOperationFilter>();
Note that the above line will replace the (presumably) existing call to options.AddSecurityRequirement(...) in the same place.

Related

Blazor Server GraphServiceClient not authenticated in wrapper library

I have a Blazor Server application that authenticates users through AAD. I'm using the Microsoft Graph SDK to query user info such as photo, etc., which I can do using DI to get the GraphServiceClient directly within a component in the Blazor Server app.
[Inject] private GraphServiceClient GraphServiceClient { get; set; }
However, for testing purposes, I've created a wrapper around the GraphServiceClient called IGraphService in another library within the same solution. This is where the problem occurs and the GraphServiceClient fails authentication with:
No account or login hint was passed to the AcquireTokenSilent call
My code is set up as follows:
Program.cs
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetValue<string>("Graph:Scopes")?.Split(' '))
.AddMicrosoftGraph(builder.Configuration.GetSection("Graph"))
.AddInMemoryTokenCaches();
builder.Services.AddScoped<IGraphService, GraphService>();
appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com",
"Domain": "{domain}.onmicrosoft.com",
"CallbackPath": "/signin-oidc",
"TenantId": "{tenantId}",
"ClientId": "{clientId}",
"ClientSecret": "{clientSecret}"
},
"Graph": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "user.read user.readbasic.all"
},
The part of the implementation of the GraphService I created to wrap the GraphServiceClient that tries to set up the GraphServiceClient:
return new GraphServiceClient(new DelegateAuthenticationProvider((requestMessage) =>
{
string[] scopes = new[] { "user.read", "user.readbasic.all" };
ITokenAcquisition tokenService = _contextAccessor.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
var token = tokenService.GetAccessTokenForUserAsync(scopes); <-- FAILS!!!
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Result);
return Task.FromResult(0);
}));
Clearing my cookies, this works fine the first time the app runs, but on restarts, it fails with the same message. Injecting the GraphServiceClient directly into a component works fine too, but I'm trying to wrap it so the components are loosely coupled.
Additionally, if I pass the AccessToken from the Blazor Server project to the wrapper library, it works fine too, but I want to avoid that.
What am I missing? Is my setup not right? Thanks for any help or pointers :)
Update 1
To simplify the issue, I have a component on a page and I can successfully request the user's token in that component, every time.
public sealed partial class HeaderBar
{
[Inject] private ITokenAcquisition TokenAcquisition { get; set; } = null!;
[Inject] private IGraphService GraphService { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
try
{
// this works every time!
string token = await TokenAcquisition.GetAccessTokenForUserAsync(new[] { "user.read", "user.readbasic.all" });
var photo = await GraphService.GetUserPhoto();
...
}
catch (Exception ex)
{
...
}
}
}
As you can see, I'm not doing anything with the token, but if I remove that line, the same line within the GraphService fails. It will only succeed and get a token if I make the same call within the component first.
I've tried moving the code to the App.razor, but it fails there too.
Thanks to the following issue, I was able to pinpoint the problem and resolve it:
https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/issues/281
Problem:
When you lose the in-memory cache while your cookies are still alive, the auth calls are skipped and you don't bootstrap the token cache, which throws the MsalUIException described in the original question above. That explains why everything works the first time the app runs and doesn't when it gets restarted.
Some of the solutions describe using an attribute to re-populate the cache. However, in Blazor Server, there is no "Controller", so we can't use the AuthorizeForScopes attribute.
Solution:
I've created a page model for the standard _Host.csthml page that will attempt to get a token from the token cache, and if it's not populated, the AuthorizeForScopes attribute will do that for me (Thanks wmgdev!).
[AuthorizeForScopes(Scopes = new[] { "user.read", "user.readbasic.all" })]
public class _Host : PageModel
{
private readonly ITokenAcquisition _tokenAcquisition;
public _Host(ITokenAcquisition tokenAcquisition)
{
_tokenAcquisition = tokenAcquisition;
}
public async Task OnGetAsync()
{
// Get a token. If the token cache is not populated, the `AuthorizeForScopes` attribute will re-authorize, which will populate the cache
await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "user.read", "user.readbasic.all" });
}
}

Swashbukle doesn't show the Odata routes in the Swagger UI

I am using Net6 web api with odata support. I am not using any apicontroller in the code and instead i am inheriting from ODataController and swagger UI is not showing the routes in the UI and event i am not able to browse those endpoints separately. Below is my samplecode
public class ValuesController : ODataController
{
[EnableQuery(PageSize = 5)]
public IQueryable<Note> Get()
{
return _context.Notes.AsQueryable();
}
}
Middleware configuration
builder.Services.AddControllers()
.AddOData(opt =>
{
opt.Conventions.Remove(opt.Conventions.OfType<MetadataRoutingConvention>()
.First());
opt.AddRouteComponents(GetEdmModel())
.Select()
.Expand()
.Count()
.Filter()
.OrderBy().SetMaxTop(100).TimeZone = TimeZoneInfo.Utc;
}).AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("Notes",
new Microsoft.OpenApi.Models.OpenApiInfo { Title = "Notes API", Version = "v1", });
});
Please note I have one controller with same config and it is showing in the swagger UI, if I add new controllers inheriting from ODataController it is not working. any help appreciated.
Thanks,
Suresh

How to hide actual ClientId from swagger while doing AzureAd token authentication in WebAPI

I have configured AzureAd token authentication for my webAPI but in swagger page its showing the actual clientId value but I don't want to show the actual value of ClientId to the end user. That means in the code I can hardcode but in the swagger page I want to show some dummy value, how that can be done?
In the clientId textbox, I want to pass any random value like 'swaggerClient'
services.AddSwaggerGen(op =>
{
var openApi = new OpenApiSecurityScheme
{
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = "https://abcde.com",
Scopes = new Dictionary<string, string>
{
{ Scope, "mvc1"}
},
TokenUrl = "https://abcde.com/token"
}
},
In = ParameterLocation.Header,
Name = "Authorization",
Type = SecuritySchemeType.OAuth2
};
op.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2"
}
},
new string[] {}
}
});
options.AddSecurityDefinition("oauth2", openApi);
options.OperationFilter<SecurityRequirementsOperationFilter>();
});
Client_id is not hided as that it is not a secret as OAuth specification RFC 6749 - The OAuth 2.0 Authorization Framework indicates and it is exposed to the resource owner and must be used along with client secret for client authentication and secret is hided anyway.
Please check if the parameter can be hidden by using SchemaFilter with IgnoreDataMember something like C# ASP.NET : Hide model properties from Swagger doc - DEV Community
GET the client id from getter and setter class
[IgnoreDataMember]
public string ClientId { set; get; }
use ISchemaFilter to control it. Add new class.
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
namespace swaggertest
{
public class MySwaggerSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
var ignoreDataMemberProperties = context.Type.GetProperties()
.Where(t => t.GetCustomAttribute<IgnoreDataMemberAttribute>() != null);
foreach (var ignoreDataMemberProperty in ignoreDataMemberProperties)
{
var propertyToHide = schema.Properties.Keys
.SingleOrDefault(x => x.ToLower() == ignoreDataMemberProperty.Name.ToLower());
if (propertyToHide != null)
{
schema.Properties.Remove(propertyToHide);
}
}
}
}
}
And then specifying in the start up
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "swaggertest", Version = "v1" });
c.SchemaFilter<MySwaggerSchemaFilter>();
});
...
}
Or by changing the DOM properties as suggested by kievu in github issue
You can raise a azure support request or swagger support regarding the same.
References:
Testing Azure AD-protected APIs, part 1: Swagger UI - Joonas W's
blog
How To Swagger Hide API Or Route Method – Guidelines |TheCodeBuzz

How to show c# validation attributes on query parameters in Swagger

I'm using Swagger with ASP.Net Core 2.1 Web API project. Here's an example controller action method:
[HttpGet]
public string GetString([Required, MaxLength(20)] string name) =>
$"Hi there, {name}.";
And here's what I get in the Swagger documentation. As you can see, Swagger shows the Required attribute, but not the MaxLength one:
If I use Required and MaxLength attributes on a DTO class that's the argument of a POST action method, then Swagger shows them both:
How can I get Swagger to show MaxLength (and other) validation attributes for query parameters?
Note: I have tried to replace the string name argument with a class that has one string property called name - Swagger produces exactly the same documentation.
In .NET Core, you can use ShowCommonExtensions = true, with given sequence (ConfigObject on top).
public static IApplicationBuilder UseR6SwaggerDocumentationUI(
this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
//Allow to add addition attribute info on doc. like [MaxLength(50)]
c.ConfigObject = new ConfigObject
{
ShowCommonExtensions = true
};
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Asptricks.net API");
c.RoutePrefix = "api_documentation/index";
c.InjectStylesheet("/swagger-ui/custom.css");
c.InjectJavascript("/swagger-ui/custom.js");
c.SupportedSubmitMethods( new[] { SubmitMethod.Patch });
//Collapse model near example.
c.DefaultModelExpandDepth(0);
//Remove separate model definition.
c.DefaultModelsExpandDepth(-1);
});
return app;
}

ASP.Net MVC 6 + WebAPI Auth - Redirect MVC to logon but 401 if WebAPI

I have a AngularJS + MVC + WebAPI where I'm trying to:
- Use standard (individual accounts) for MVC authentication;
- Use those same users and password for WebAPI based authentication.
Problem, from AngularJS everything works fine, the cookie exchange happens, and Web API returns the value, but when I'm trying to access the WebAPI from Postman, I get a redirect to logon page instead of a 401 Unauthorized.
What is the easiest way to achieve this? Do I have to subclass Authorize and implement the logic manually?
Thank you
For the ASP.Net 5 latest beta8, the answer is to add the following to ConfigureServices on Startup.cs:
services.Configure<IdentityOptions>(config =>
{
options.Cookies.ApplicationCookie.LoginPath = "/Account/Login";
options.Cookies.ApplicationCookie.CookieHttpOnly = true;
options.Cookies.ApplicationCookie.CookieSecure = CookieSecureOption.SameAsRequest;
options.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents()
{
OnRedirect = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api") &&
ctx.Response.StatusCode == 200)
{
ctx.Response.StatusCode = 401;
return Task.FromResult<object>(null);
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
return Task.FromResult<object>(null);
}
}
};
});
You could simply apply a custom action for Redirect event. On App_Start/Startup.Auth.cs file find app.UseCookieAuthentication() method and alter like this:
public void ConfigureAuth(IAppBuilder app)
{
// some omitted configurations
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// some omitted configurations
Provider = new CookieAuthenticationProvider
{
// some omitted configurations
OnApplyRedirect = context =>
{
// assuming your API's url starts with /api
if(!context.Request.Path.StartsWithSegments(new PathString("/api")))
context.Response.Redirect(context.RedirectUri);
}
}
});
}
In RC1-Final (VS2015.1) I've done with the following:
in Identity configuration set AutomaticChallenge to false and ApplicationCookieAuthenticationScheme = "ApplicationCookie":
services.AddIdentity<AppUser>(options =>
{
// cut
options.Cookies.ApplicationCookie.AutomaticAuthenticate = true;
options.Cookies.ApplicationCookie.AutomaticChallenge = false;
options.Cookies.ApplicationCookieAuthenticationScheme = "ApplicationCookie";
})
.AddUserStore<AppUserStore<AppUser, AppDbContext>>()
.AddDefaultTokenProviders();
Then controllers, that I want to redirect to login, I add ActiveAuthenticationSchemes = "ApplicationCookie"
[Authorize(ActiveAuthenticationSchemes = "ApplicationCookie")]
public async Task<IActionResult> Logout()
{
// cut
}
but other controllers (WebAPI in my case) I marked with parameter less Authorize attribute.
From AuthenticationOptions.cs inline help for AutomaticChallenge:
If false the authentication middleware will only alter responses when explicitly indicated by the AuthenticationScheme.

Resources